analytica-frontend-lib 1.7.21 → 1.7.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AccessReportModal/index.js +3 -3
- package/dist/AccessReportModal/index.mjs +2 -2
- package/dist/ActivitiesHistory/index.js +49 -49
- package/dist/ActivitiesHistory/index.mjs +48 -48
- package/dist/ActivityCardQuestionBanks/index.js +49 -49
- package/dist/ActivityCardQuestionBanks/index.mjs +48 -48
- package/dist/ActivityCardQuestionPreview/index.js +49 -49
- package/dist/ActivityCardQuestionPreview/index.mjs +48 -48
- package/dist/ActivityCreate/ActivityCreateContent/index.js +49 -49
- package/dist/ActivityCreate/ActivityCreateContent/index.mjs +48 -48
- package/dist/ActivityCreate/index.js +49 -49
- package/dist/ActivityCreate/index.mjs +48 -48
- package/dist/ActivityDetails/index.js +49 -49
- package/dist/ActivityDetails/index.mjs +48 -48
- package/dist/ActivityFilters/index.js +49 -49
- package/dist/ActivityFilters/index.mjs +48 -48
- package/dist/ActivityPageLayout/index.js +49 -49
- package/dist/ActivityPageLayout/index.mjs +48 -48
- package/dist/ActivityPreview/index.js +49 -49
- package/dist/ActivityPreview/index.mjs +48 -48
- package/dist/AlertManager/index.js +49 -49
- package/dist/AlertManager/index.mjs +48 -48
- package/dist/AlertManagerView/index.js +3 -3
- package/dist/AlertManagerView/index.mjs +2 -2
- package/dist/AnnouncementModal/index.js +3 -3
- package/dist/AnnouncementModal/index.mjs +2 -2
- package/dist/AnnouncementsGate/index.js +4 -4
- package/dist/AnnouncementsGate/index.mjs +3 -3
- package/dist/AnswerKeyModal/index.js +3 -3
- package/dist/AnswerKeyModal/index.mjs +2 -2
- package/dist/AppHeader/index.js +6 -6
- package/dist/AppHeader/index.mjs +5 -5
- package/dist/CalendarCard/index.js +4 -4
- package/dist/CalendarCard/index.mjs +3 -3
- package/dist/Chat/index.js +3 -3
- package/dist/Chat/index.mjs +2 -2
- package/dist/CheckBox/index.d.ts.map +1 -1
- package/dist/CheckBox/index.js +2 -2
- package/dist/CheckBox/index.mjs +1 -1
- package/dist/CheckBoxGroup/index.js +49 -49
- package/dist/CheckBoxGroup/index.mjs +48 -48
- package/dist/ComparatorChart/ComparatorView/index.js +3 -3
- package/dist/ComparatorChart/ComparatorView/index.mjs +2 -2
- package/dist/CorrectActivityModal/index.js +7 -7
- package/dist/CorrectActivityModal/index.mjs +6 -6
- package/dist/DownloadButton/index.js +4 -4
- package/dist/DownloadButton/index.mjs +3 -3
- package/dist/DownloadModal/index.js +3 -3
- package/dist/DownloadModal/index.mjs +2 -2
- package/dist/DropdownMenu/index.js +3 -3
- package/dist/DropdownMenu/index.mjs +2 -2
- package/dist/EnemClassroomStartModal/index.d.ts.map +1 -1
- package/dist/EnemClassroomStartModal/index.js +7 -7
- package/dist/EnemClassroomStartModal/index.mjs +6 -6
- package/dist/EnemMoment/index.js +57 -57
- package/dist/EnemMoment/index.mjs +51 -51
- package/dist/EnemMomentStudentModal/index.js +50 -50
- package/dist/EnemMomentStudentModal/index.mjs +48 -48
- package/dist/ExamDetailsLayout/ExamDetailsPage/index.js +49 -49
- package/dist/ExamDetailsLayout/ExamDetailsPage/index.mjs +48 -48
- package/dist/ExamPageLayout/index.js +49 -49
- package/dist/ExamPageLayout/index.mjs +48 -48
- package/dist/Forum/index.js +5 -5
- package/dist/Forum/index.mjs +4 -4
- package/dist/KnowledgePerformanceCard/index.js +51 -51
- package/dist/KnowledgePerformanceCard/index.mjs +50 -50
- package/dist/LessonCardPreview/index.js +49 -49
- package/dist/LessonCardPreview/index.mjs +48 -48
- package/dist/LessonMediaSections/index.js +49 -49
- package/dist/LessonMediaSections/index.mjs +48 -48
- package/dist/LessonsCatalog/index.js +49 -49
- package/dist/LessonsCatalog/index.mjs +48 -48
- package/dist/Modal/index.d.ts.map +1 -1
- package/dist/Modal/index.js +2 -2
- package/dist/Modal/index.mjs +1 -1
- package/dist/MultiCheckSelect/index.js +5 -5
- package/dist/MultiCheckSelect/index.mjs +4 -4
- package/dist/MultiSearchSelect/index.js +3 -3
- package/dist/MultiSearchSelect/index.mjs +2 -2
- package/dist/MultipleChoice/index.js +3 -3
- package/dist/MultipleChoice/index.mjs +2 -2
- package/dist/NotificationCard/index.js +4 -4
- package/dist/NotificationCard/index.mjs +3 -3
- package/dist/PerformanceReportModal/index.js +3 -3
- package/dist/PerformanceReportModal/index.mjs +2 -2
- package/dist/PrintableUsersTable/index.js +49 -49
- package/dist/PrintableUsersTable/index.mjs +48 -48
- package/dist/ProgressModal/index.js +3 -3
- package/dist/ProgressModal/index.mjs +2 -2
- package/dist/QuestionsPerformanceCard/index.js +50 -50
- package/dist/QuestionsPerformanceCard/index.mjs +49 -49
- package/dist/Quiz/index.js +6 -6
- package/dist/Quiz/index.mjs +5 -5
- package/dist/RecommendedLessonCreate/index.js +49 -49
- package/dist/RecommendedLessonCreate/index.mjs +48 -48
- package/dist/RecommendedLessonDetails/index.js +7 -7
- package/dist/RecommendedLessonDetails/index.mjs +6 -6
- package/dist/RecommendedLessonsHistory/index.js +49 -49
- package/dist/RecommendedLessonsHistory/index.mjs +48 -48
- package/dist/ReportDetailModal/index.js +4 -4
- package/dist/ReportDetailModal/index.mjs +3 -3
- package/dist/RichEditor/index.js +5 -5
- package/dist/RichEditor/index.mjs +4 -4
- package/dist/{RichEditorCore-6MTYSSKS.mjs → RichEditorCore-2S5YUAOI.mjs} +5 -5
- package/dist/{RichEditorCore-KDARBEK4.js → RichEditorCore-N7A2445B.js} +16 -16
- package/dist/{RichEditorCore-KDARBEK4.js.map → RichEditorCore-N7A2445B.js.map} +1 -1
- package/dist/Search/index.js +4 -4
- package/dist/Search/index.mjs +3 -3
- package/dist/SendActivityModal/SendActivityModal.js +49 -49
- package/dist/SendActivityModal/SendActivityModal.mjs +48 -48
- package/dist/SendActivityModal/index.js +49 -49
- package/dist/SendActivityModal/index.mjs +48 -48
- package/dist/SendLessonModal/index.js +49 -49
- package/dist/SendLessonModal/index.mjs +48 -48
- package/dist/SimulatedPerformance/SimulatedPerformanceView/index.js +49 -49
- package/dist/SimulatedPerformance/SimulatedPerformanceView/index.mjs +48 -48
- package/dist/SimulatedPerformance/useSimulatedPerformance/index.js +5 -5
- package/dist/SimulatedPerformance/useSimulatedPerformance/index.mjs +4 -4
- package/dist/SimulatedStudentSimulationsModal/index.js +6 -6
- package/dist/SimulatedStudentSimulationsModal/index.mjs +5 -5
- package/dist/SimulationsPage/index.js +49 -49
- package/dist/SimulationsPage/index.mjs +48 -48
- package/dist/StudentLessonProgressModal/index.js +3 -3
- package/dist/StudentLessonProgressModal/index.mjs +2 -2
- package/dist/StudentPerformanceDetailsModal/index.js +5 -5
- package/dist/StudentPerformanceDetailsModal/index.mjs +4 -4
- package/dist/Support/TicketModal/index.js +3 -3
- package/dist/Support/TicketModal/index.mjs +2 -2
- package/dist/Support/index.js +4 -4
- package/dist/Support/index.mjs +3 -3
- package/dist/SupportChatComposer/index.js +3 -3
- package/dist/SupportChatComposer/index.mjs +2 -2
- package/dist/SupportChatEvent/index.js +3 -3
- package/dist/SupportChatEvent/index.mjs +2 -2
- package/dist/SupportChatMessage/index.js +3 -3
- package/dist/SupportChatMessage/index.mjs +2 -2
- package/dist/SupportChatWidget/index.js +3 -3
- package/dist/SupportChatWidget/index.mjs +2 -2
- package/dist/SupportConcludeModal/index.js +4 -4
- package/dist/SupportConcludeModal/index.mjs +3 -3
- package/dist/SupportConversationHeader/index.js +4 -4
- package/dist/SupportConversationHeader/index.mjs +3 -3
- package/dist/SupportConversationMessages/index.js +5 -5
- package/dist/SupportConversationMessages/index.mjs +4 -4
- package/dist/SupportTicketConversation/index.js +12 -12
- package/dist/SupportTicketConversation/index.mjs +11 -11
- package/dist/TableProvider/index.js +49 -49
- package/dist/TableProvider/index.mjs +48 -48
- package/dist/UnifiedDraftModelPage/index.js +49 -49
- package/dist/UnifiedDraftModelPage/index.mjs +48 -48
- package/dist/UnifiedHistoryPage/index.js +49 -49
- package/dist/UnifiedHistoryPage/index.mjs +48 -48
- package/dist/VideoPlayer/index.js +5 -5
- package/dist/VideoPlayer/index.mjs +4 -4
- package/dist/Whiteboard/index.js +3 -3
- package/dist/Whiteboard/index.mjs +2 -2
- package/dist/{chunk-B6QOBHZM.mjs → chunk-24BEZK43.mjs} +2 -2
- package/dist/{chunk-CYSODZGX.js → chunk-27Y6V3GM.js} +169 -66
- package/dist/chunk-27Y6V3GM.js.map +1 -0
- package/dist/{chunk-OQTXTDSK.mjs → chunk-2OJ3QSLU.mjs} +3 -3
- package/dist/{chunk-VDUO65VK.mjs → chunk-3EGJJDIM.mjs} +2 -2
- package/dist/{chunk-VDUO65VK.mjs.map → chunk-3EGJJDIM.mjs.map} +1 -1
- package/dist/{chunk-4OUBPEVS.mjs → chunk-3HKLGNUE.mjs} +3 -3
- package/dist/{chunk-YA7XLH7D.js → chunk-3MFACEZ2.js} +3 -3
- package/dist/{chunk-YA7XLH7D.js.map → chunk-3MFACEZ2.js.map} +1 -1
- package/dist/{chunk-IHBWJM4C.mjs → chunk-3Q7RJKUP.mjs} +2 -2
- package/dist/{chunk-PY6WD6SB.mjs → chunk-3WRB4WCD.mjs} +4 -4
- package/dist/{chunk-4RHCBMMP.js → chunk-46VHAL4S.js} +3 -3
- package/dist/{chunk-4RHCBMMP.js.map → chunk-46VHAL4S.js.map} +1 -1
- package/dist/{chunk-POT3ALY3.js → chunk-5DINXU6W.js} +1 -1
- package/dist/{chunk-POT3ALY3.js.map → chunk-5DINXU6W.js.map} +1 -1
- package/dist/{chunk-L52NFNFK.mjs → chunk-5VC4BK65.mjs} +3 -3
- package/dist/{chunk-B67RLIUM.js → chunk-72D7G6YQ.js} +3 -3
- package/dist/{chunk-B67RLIUM.js.map → chunk-72D7G6YQ.js.map} +1 -1
- package/dist/{chunk-C65DG2RI.mjs → chunk-7FSBWUOM.mjs} +2 -2
- package/dist/{chunk-QWMFWYII.js → chunk-7IZNLARX.js} +3 -3
- package/dist/{chunk-QWMFWYII.js.map → chunk-7IZNLARX.js.map} +1 -1
- package/dist/{chunk-ZCBT2BUZ.js → chunk-7RFHXFFV.js} +13 -13
- package/dist/{chunk-ZCBT2BUZ.js.map → chunk-7RFHXFFV.js.map} +1 -1
- package/dist/{chunk-EMWX6IKP.js → chunk-7VJP5D26.js} +13 -13
- package/dist/{chunk-EMWX6IKP.js.map → chunk-7VJP5D26.js.map} +1 -1
- package/dist/{chunk-HCQJR5CE.mjs → chunk-7ZWEBNFB.mjs} +2 -2
- package/dist/{chunk-3RHNJUXU.mjs → chunk-ACSEJOW6.mjs} +2 -2
- package/dist/{chunk-WQZHUW7C.mjs → chunk-ANFHLSDB.mjs} +3 -3
- package/dist/{chunk-H6NQOLQY.mjs → chunk-ANFLWCVD.mjs} +2 -2
- package/dist/{chunk-BKBDQXZX.js → chunk-AOYPUZRO.js} +3 -3
- package/dist/{chunk-BKBDQXZX.js.map → chunk-AOYPUZRO.js.map} +1 -1
- package/dist/{chunk-5WS6IENQ.mjs → chunk-AU2HLQDJ.mjs} +2 -2
- package/dist/{chunk-P6FX3NT6.mjs → chunk-AZGDHAP5.mjs} +2 -2
- package/dist/{chunk-NCGO7FPB.js → chunk-AZKNYREH.js} +8 -8
- package/dist/{chunk-NCGO7FPB.js.map → chunk-AZKNYREH.js.map} +1 -1
- package/dist/{chunk-R6MTPYYW.js → chunk-AZTBNERQ.js} +3 -3
- package/dist/{chunk-R6MTPYYW.js.map → chunk-AZTBNERQ.js.map} +1 -1
- package/dist/{chunk-NPB2JTYC.mjs → chunk-BO3QVQ37.mjs} +4 -4
- package/dist/{chunk-2WI4Q2I2.mjs → chunk-BPMWRFSI.mjs} +4 -4
- package/dist/{chunk-TEW33GLR.mjs → chunk-BXN3VO6I.mjs} +2 -2
- package/dist/{chunk-7ZB3YHF2.mjs → chunk-C6LC5LD5.mjs} +1 -1
- package/dist/{chunk-7ZB3YHF2.mjs.map → chunk-C6LC5LD5.mjs.map} +1 -1
- package/dist/{chunk-5HONWYQA.js → chunk-C7ILTLT5.js} +3 -3
- package/dist/{chunk-5HONWYQA.js.map → chunk-C7ILTLT5.js.map} +1 -1
- package/dist/{chunk-OES54VU6.mjs → chunk-CFV2OZ3P.mjs} +116 -13
- package/dist/chunk-CFV2OZ3P.mjs.map +1 -0
- package/dist/{chunk-4OVIHZ3I.js → chunk-CGXWWPEA.js} +6 -6
- package/dist/{chunk-4OVIHZ3I.js.map → chunk-CGXWWPEA.js.map} +1 -1
- package/dist/{chunk-MNU45FM3.js → chunk-COT4KW3H.js} +2 -2
- package/dist/{chunk-MNU45FM3.js.map → chunk-COT4KW3H.js.map} +1 -1
- package/dist/{chunk-JDQUXG75.js → chunk-CVDNCU6L.js} +3 -3
- package/dist/{chunk-JDQUXG75.js.map → chunk-CVDNCU6L.js.map} +1 -1
- package/dist/{chunk-HBZHWSMZ.mjs → chunk-D46PJSQ7.mjs} +13 -13
- package/dist/{chunk-ARYIJ4RU.js → chunk-DDORKDQO.js} +3 -3
- package/dist/{chunk-ARYIJ4RU.js.map → chunk-DDORKDQO.js.map} +1 -1
- package/dist/{chunk-7YQTDJFA.js → chunk-DKXSR2V3.js} +3 -3
- package/dist/{chunk-7YQTDJFA.js.map → chunk-DKXSR2V3.js.map} +1 -1
- package/dist/{chunk-I6EAPPBL.mjs → chunk-DZMYIOKZ.mjs} +5 -5
- package/dist/{chunk-SKM7NFPY.js → chunk-EPXAFJVW.js} +6 -6
- package/dist/{chunk-SKM7NFPY.js.map → chunk-EPXAFJVW.js.map} +1 -1
- package/dist/{chunk-AYYMS23L.js → chunk-ESIQ2GB4.js} +7 -7
- package/dist/{chunk-AYYMS23L.js.map → chunk-ESIQ2GB4.js.map} +1 -1
- package/dist/{chunk-NU2WNYOD.js → chunk-EW7HGUFV.js} +87 -87
- package/dist/{chunk-NU2WNYOD.js.map → chunk-EW7HGUFV.js.map} +1 -1
- package/dist/{chunk-I4QGQMXI.mjs → chunk-EXYXZFNK.mjs} +2 -2
- package/dist/{chunk-RBJAO57A.js → chunk-FAAR4XC5.js} +3 -3
- package/dist/{chunk-RBJAO57A.js.map → chunk-FAAR4XC5.js.map} +1 -1
- package/dist/{chunk-N2VNE3ON.js → chunk-FDTDJDZI.js} +13 -13
- package/dist/{chunk-N2VNE3ON.js.map → chunk-FDTDJDZI.js.map} +1 -1
- package/dist/{chunk-SU5Q64TW.mjs → chunk-FVCTKS33.mjs} +6 -5
- package/dist/chunk-FVCTKS33.mjs.map +1 -0
- package/dist/{chunk-AVHZHWI3.js → chunk-GBIFSFWJ.js} +15 -15
- package/dist/{chunk-AVHZHWI3.js.map → chunk-GBIFSFWJ.js.map} +1 -1
- package/dist/{chunk-KSUPPAKH.js → chunk-GIQSHTB5.js} +9 -9
- package/dist/{chunk-KSUPPAKH.js.map → chunk-GIQSHTB5.js.map} +1 -1
- package/dist/{chunk-4QBKD4JN.js → chunk-GUUJYGXF.js} +3 -3
- package/dist/{chunk-4QBKD4JN.js.map → chunk-GUUJYGXF.js.map} +1 -1
- package/dist/{chunk-M4O46VDY.mjs → chunk-GX2PKLN2.mjs} +2 -2
- package/dist/{chunk-5BUTINIM.mjs → chunk-HLVLSWKD.mjs} +3 -3
- package/dist/{chunk-FU23WOTY.js → chunk-HSV5VZ4Z.js} +6 -6
- package/dist/{chunk-FU23WOTY.js.map → chunk-HSV5VZ4Z.js.map} +1 -1
- package/dist/{chunk-MMS2ELDK.mjs → chunk-IIQCS3A2.mjs} +2 -2
- package/dist/{chunk-NKWLCZUZ.mjs → chunk-JGGUMHM6.mjs} +2 -2
- package/dist/{chunk-HT2GREFH.js → chunk-JLD46DL7.js} +11 -11
- package/dist/{chunk-HT2GREFH.js.map → chunk-JLD46DL7.js.map} +1 -1
- package/dist/{chunk-NVL5TKWD.mjs → chunk-JYFLOQYT.mjs} +2 -2
- package/dist/{chunk-DWGDEUEW.mjs → chunk-KG563P7F.mjs} +5 -5
- package/dist/{chunk-TCIC5556.mjs → chunk-KH4TEDGD.mjs} +2 -2
- package/dist/{chunk-23PAYKWB.js → chunk-KM27ZDHZ.js} +2 -2
- package/dist/{chunk-23PAYKWB.js.map → chunk-KM27ZDHZ.js.map} +1 -1
- package/dist/{chunk-XZUMD3CB.mjs → chunk-LODDYDTP.mjs} +2 -2
- package/dist/{chunk-XZUMD3CB.mjs.map → chunk-LODDYDTP.mjs.map} +1 -1
- package/dist/{chunk-E6CGJBID.mjs → chunk-LUHKMY56.mjs} +2 -2
- package/dist/{chunk-TVLEUNKE.mjs → chunk-LZP5VZCO.mjs} +2 -2
- package/dist/{chunk-EHNEBXS7.mjs → chunk-M6DXZ2FM.mjs} +2 -2
- package/dist/{chunk-B4C5DWAP.mjs → chunk-MMMJDO75.mjs} +2 -2
- package/dist/{chunk-AFCWMJQO.js → chunk-MOUPNP53.js} +3 -3
- package/dist/{chunk-AFCWMJQO.js.map → chunk-MOUPNP53.js.map} +1 -1
- package/dist/{chunk-36KFIZ43.js → chunk-MYPUZ6EG.js} +3 -3
- package/dist/{chunk-36KFIZ43.js.map → chunk-MYPUZ6EG.js.map} +1 -1
- package/dist/{chunk-6JHIKIPA.js → chunk-NUGLTWEA.js} +23 -23
- package/dist/{chunk-6JHIKIPA.js.map → chunk-NUGLTWEA.js.map} +1 -1
- package/dist/{chunk-KWXOYHYK.mjs → chunk-P7MORXSE.mjs} +2 -2
- package/dist/{chunk-YG4IZ5Q3.mjs → chunk-Q2OSEROT.mjs} +2 -2
- package/dist/{chunk-L5SXG6T6.mjs → chunk-QQKOHKO2.mjs} +3 -3
- package/dist/{chunk-SADSJ2MV.js → chunk-QVGUYPXO.js} +3 -3
- package/dist/{chunk-SADSJ2MV.js.map → chunk-QVGUYPXO.js.map} +1 -1
- package/dist/{chunk-K45PTE7F.mjs → chunk-RBKQXT73.mjs} +4 -4
- package/dist/{chunk-JZFISWTP.js → chunk-RIN2IN7E.js} +2 -2
- package/dist/{chunk-JZFISWTP.js.map → chunk-RIN2IN7E.js.map} +1 -1
- package/dist/{chunk-BXLAOYAG.mjs → chunk-RIUVPTG7.mjs} +2 -2
- package/dist/{chunk-QVB54LBS.mjs → chunk-RNZGX6MX.mjs} +3 -3
- package/dist/{chunk-VSVLIRO2.js → chunk-RXEU77VN.js} +15 -15
- package/dist/{chunk-VSVLIRO2.js.map → chunk-RXEU77VN.js.map} +1 -1
- package/dist/{chunk-ZRIYCU2V.js → chunk-S2BZPPAI.js} +5 -5
- package/dist/{chunk-ZRIYCU2V.js.map → chunk-S2BZPPAI.js.map} +1 -1
- package/dist/{chunk-MES2BP4T.mjs → chunk-SGL5IUCV.mjs} +2 -2
- package/dist/{chunk-7LS5UQCQ.mjs → chunk-STKUPDHG.mjs} +2 -2
- package/dist/{chunk-PPYCCT2K.js → chunk-SWDVSCQJ.js} +9 -8
- package/dist/chunk-SWDVSCQJ.js.map +1 -0
- package/dist/{chunk-JCA76TCX.mjs → chunk-SZKLJOTN.mjs} +2 -2
- package/dist/{chunk-F5WR6XZZ.js → chunk-T73HPYF2.js} +3 -3
- package/dist/{chunk-F5WR6XZZ.js.map → chunk-T73HPYF2.js.map} +1 -1
- package/dist/{chunk-ZB6HDDE4.js → chunk-TQO37PVW.js} +14 -14
- package/dist/{chunk-ZB6HDDE4.js.map → chunk-TQO37PVW.js.map} +1 -1
- package/dist/{chunk-VKQ5AJDT.mjs → chunk-UENZQF2Y.mjs} +2 -2
- package/dist/{chunk-DCG7EUBC.js → chunk-UWLW5YVB.js} +6 -6
- package/dist/{chunk-DCG7EUBC.js.map → chunk-UWLW5YVB.js.map} +1 -1
- package/dist/{chunk-37AJ2GVO.mjs → chunk-UXJIZDXQ.mjs} +2 -2
- package/dist/{chunk-PLLPFCN3.js → chunk-UXMDPRAI.js} +14 -14
- package/dist/{chunk-PLLPFCN3.js.map → chunk-UXMDPRAI.js.map} +1 -1
- package/dist/{chunk-7BOTKRY6.mjs → chunk-V55DG5B4.mjs} +2 -2
- package/dist/{chunk-YRTIDZTN.js → chunk-VHEDMUAW.js} +4 -4
- package/dist/{chunk-YRTIDZTN.js.map → chunk-VHEDMUAW.js.map} +1 -1
- package/dist/{chunk-4RD55E4R.js → chunk-VWLWSO4J.js} +3 -3
- package/dist/{chunk-4RD55E4R.js.map → chunk-VWLWSO4J.js.map} +1 -1
- package/dist/{chunk-VJQIKFMG.mjs → chunk-W3CWEZDB.mjs} +3 -3
- package/dist/{chunk-JM6SLU7D.js → chunk-W7NNH4VY.js} +6 -6
- package/dist/{chunk-JM6SLU7D.js.map → chunk-W7NNH4VY.js.map} +1 -1
- package/dist/{chunk-VQ7MSYIH.js → chunk-WUN5NHZE.js} +4 -4
- package/dist/{chunk-VQ7MSYIH.js.map → chunk-WUN5NHZE.js.map} +1 -1
- package/dist/{chunk-AAU7ABJN.js → chunk-WWTRSQ3X.js} +3 -3
- package/dist/{chunk-AAU7ABJN.js.map → chunk-WWTRSQ3X.js.map} +1 -1
- package/dist/{chunk-BR6D2TGF.js → chunk-WZOA2PVJ.js} +3 -3
- package/dist/{chunk-BR6D2TGF.js.map → chunk-WZOA2PVJ.js.map} +1 -1
- package/dist/{chunk-TZKR5Z4J.mjs → chunk-XJF2F36N.mjs} +2 -2
- package/dist/{chunk-P6ACVNQK.mjs → chunk-YD2XBEKO.mjs} +2 -2
- package/dist/{chunk-LMXWIPGR.js → chunk-YWPLHFAK.js} +3 -3
- package/dist/{chunk-LMXWIPGR.js.map → chunk-YWPLHFAK.js.map} +1 -1
- package/dist/{chunk-DCEWWHV5.mjs → chunk-Z3HGYPH7.mjs} +2 -2
- package/dist/{chunk-Q3PKW6TH.js → chunk-ZCV3HWI2.js} +6 -6
- package/dist/{chunk-Q3PKW6TH.js.map → chunk-ZCV3HWI2.js.map} +1 -1
- package/dist/{chunk-OSA3YVDZ.mjs → chunk-ZF46DCAC.mjs} +2 -2
- package/dist/{chunk-5T6IDXSB.js → chunk-ZO2GA23I.js} +3 -3
- package/dist/{chunk-5T6IDXSB.js.map → chunk-ZO2GA23I.js.map} +1 -1
- package/dist/{chunk-ZN3763VZ.js → chunk-ZQLW4EJB.js} +9 -9
- package/dist/{chunk-ZN3763VZ.js.map → chunk-ZQLW4EJB.js.map} +1 -1
- package/dist/{chunk-CM5R4SPF.js → chunk-ZQQJTO6T.js} +3 -3
- package/dist/{chunk-CM5R4SPF.js.map → chunk-ZQQJTO6T.js.map} +1 -1
- package/dist/{chunk-SEL4QDCO.js → chunk-ZSXSAE3D.js} +3 -3
- package/dist/{chunk-SEL4QDCO.js.map → chunk-ZSXSAE3D.js.map} +1 -1
- package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts +6 -1
- package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts.map +1 -1
- package/dist/hooks/useAppContent/index.js +49 -49
- package/dist/hooks/useAppContent/index.mjs +48 -48
- package/dist/hooks/useSupportChat.d.ts +2 -0
- package/dist/hooks/useSupportChat.d.ts.map +1 -1
- package/dist/index.js +49 -49
- package/dist/index.mjs +48 -48
- package/dist/types/supportChat/index.d.ts +14 -0
- package/dist/types/supportChat/index.d.ts.map +1 -1
- package/dist/types/supportChat/index.js +2 -2
- package/dist/types/supportChat/index.mjs +1 -1
- package/dist/types/supportChat.d.ts +14 -0
- package/dist/types/supportChat.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CYSODZGX.js.map +0 -1
- package/dist/chunk-OES54VU6.mjs.map +0 -1
- package/dist/chunk-PPYCCT2K.js.map +0 -1
- package/dist/chunk-SU5Q64TW.mjs.map +0 -1
- /package/dist/{RichEditorCore-6MTYSSKS.mjs.map → RichEditorCore-2S5YUAOI.mjs.map} +0 -0
- /package/dist/{chunk-B6QOBHZM.mjs.map → chunk-24BEZK43.mjs.map} +0 -0
- /package/dist/{chunk-OQTXTDSK.mjs.map → chunk-2OJ3QSLU.mjs.map} +0 -0
- /package/dist/{chunk-4OUBPEVS.mjs.map → chunk-3HKLGNUE.mjs.map} +0 -0
- /package/dist/{chunk-IHBWJM4C.mjs.map → chunk-3Q7RJKUP.mjs.map} +0 -0
- /package/dist/{chunk-PY6WD6SB.mjs.map → chunk-3WRB4WCD.mjs.map} +0 -0
- /package/dist/{chunk-L52NFNFK.mjs.map → chunk-5VC4BK65.mjs.map} +0 -0
- /package/dist/{chunk-C65DG2RI.mjs.map → chunk-7FSBWUOM.mjs.map} +0 -0
- /package/dist/{chunk-HCQJR5CE.mjs.map → chunk-7ZWEBNFB.mjs.map} +0 -0
- /package/dist/{chunk-3RHNJUXU.mjs.map → chunk-ACSEJOW6.mjs.map} +0 -0
- /package/dist/{chunk-WQZHUW7C.mjs.map → chunk-ANFHLSDB.mjs.map} +0 -0
- /package/dist/{chunk-H6NQOLQY.mjs.map → chunk-ANFLWCVD.mjs.map} +0 -0
- /package/dist/{chunk-5WS6IENQ.mjs.map → chunk-AU2HLQDJ.mjs.map} +0 -0
- /package/dist/{chunk-P6FX3NT6.mjs.map → chunk-AZGDHAP5.mjs.map} +0 -0
- /package/dist/{chunk-NPB2JTYC.mjs.map → chunk-BO3QVQ37.mjs.map} +0 -0
- /package/dist/{chunk-2WI4Q2I2.mjs.map → chunk-BPMWRFSI.mjs.map} +0 -0
- /package/dist/{chunk-TEW33GLR.mjs.map → chunk-BXN3VO6I.mjs.map} +0 -0
- /package/dist/{chunk-HBZHWSMZ.mjs.map → chunk-D46PJSQ7.mjs.map} +0 -0
- /package/dist/{chunk-I6EAPPBL.mjs.map → chunk-DZMYIOKZ.mjs.map} +0 -0
- /package/dist/{chunk-I4QGQMXI.mjs.map → chunk-EXYXZFNK.mjs.map} +0 -0
- /package/dist/{chunk-M4O46VDY.mjs.map → chunk-GX2PKLN2.mjs.map} +0 -0
- /package/dist/{chunk-5BUTINIM.mjs.map → chunk-HLVLSWKD.mjs.map} +0 -0
- /package/dist/{chunk-MMS2ELDK.mjs.map → chunk-IIQCS3A2.mjs.map} +0 -0
- /package/dist/{chunk-NKWLCZUZ.mjs.map → chunk-JGGUMHM6.mjs.map} +0 -0
- /package/dist/{chunk-NVL5TKWD.mjs.map → chunk-JYFLOQYT.mjs.map} +0 -0
- /package/dist/{chunk-DWGDEUEW.mjs.map → chunk-KG563P7F.mjs.map} +0 -0
- /package/dist/{chunk-TCIC5556.mjs.map → chunk-KH4TEDGD.mjs.map} +0 -0
- /package/dist/{chunk-E6CGJBID.mjs.map → chunk-LUHKMY56.mjs.map} +0 -0
- /package/dist/{chunk-TVLEUNKE.mjs.map → chunk-LZP5VZCO.mjs.map} +0 -0
- /package/dist/{chunk-EHNEBXS7.mjs.map → chunk-M6DXZ2FM.mjs.map} +0 -0
- /package/dist/{chunk-B4C5DWAP.mjs.map → chunk-MMMJDO75.mjs.map} +0 -0
- /package/dist/{chunk-KWXOYHYK.mjs.map → chunk-P7MORXSE.mjs.map} +0 -0
- /package/dist/{chunk-YG4IZ5Q3.mjs.map → chunk-Q2OSEROT.mjs.map} +0 -0
- /package/dist/{chunk-L5SXG6T6.mjs.map → chunk-QQKOHKO2.mjs.map} +0 -0
- /package/dist/{chunk-K45PTE7F.mjs.map → chunk-RBKQXT73.mjs.map} +0 -0
- /package/dist/{chunk-BXLAOYAG.mjs.map → chunk-RIUVPTG7.mjs.map} +0 -0
- /package/dist/{chunk-QVB54LBS.mjs.map → chunk-RNZGX6MX.mjs.map} +0 -0
- /package/dist/{chunk-MES2BP4T.mjs.map → chunk-SGL5IUCV.mjs.map} +0 -0
- /package/dist/{chunk-7LS5UQCQ.mjs.map → chunk-STKUPDHG.mjs.map} +0 -0
- /package/dist/{chunk-JCA76TCX.mjs.map → chunk-SZKLJOTN.mjs.map} +0 -0
- /package/dist/{chunk-VKQ5AJDT.mjs.map → chunk-UENZQF2Y.mjs.map} +0 -0
- /package/dist/{chunk-37AJ2GVO.mjs.map → chunk-UXJIZDXQ.mjs.map} +0 -0
- /package/dist/{chunk-7BOTKRY6.mjs.map → chunk-V55DG5B4.mjs.map} +0 -0
- /package/dist/{chunk-VJQIKFMG.mjs.map → chunk-W3CWEZDB.mjs.map} +0 -0
- /package/dist/{chunk-TZKR5Z4J.mjs.map → chunk-XJF2F36N.mjs.map} +0 -0
- /package/dist/{chunk-P6ACVNQK.mjs.map → chunk-YD2XBEKO.mjs.map} +0 -0
- /package/dist/{chunk-DCEWWHV5.mjs.map → chunk-Z3HGYPH7.mjs.map} +0 -0
- /package/dist/{chunk-OSA3YVDZ.mjs.map → chunk-ZF46DCAC.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-NCGO7FPB.js","../src/utils/questionRenderer/components/index.tsx","../src/components/shared/TrueFalseStatementList.tsx","../src/utils/studentActivityCorrection/constants.ts","../src/utils/studentActivityCorrection/utils.ts","../src/utils/studentActivityCorrection/subtopics.ts","../src/utils/studentActivityCorrection/performanceTag.ts","../src/utils/studentActivityCorrection/converter.ts","../src/components/SimulationsPage/SimulationsDetailModal.tsx","../src/utils/questionDuration.ts","../src/hooks/useSimulationCardDetails.ts","../src/components/SimulatedStudentSimulationsModal/types.ts","../src/components/SimulatedStudentSimulationsModal/SimulatedStudentSimulationsModal.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-NCGO7FPB.js","sourcesContent":[null,"import { Fragment, useId, type ReactNode } from 'react';\nimport Badge from '../../../components/Badge/Badge';\nimport { CheckCircleIcon } from '@phosphor-icons/react/dist/csr/CheckCircle';\nimport { XCircleIcon } from '@phosphor-icons/react/dist/csr/XCircle';\nimport { cn } from '../../utils';\nimport Text from '../../../components/Text/Text';\nimport type { QuestionRendererProps } from '../types';\nimport { stripHtmlTags } from '../../stringUtils';\nimport { OptionStatus } from '../../../enums/Options';\n\n/**\n * Get status badge component\n */\nexport const getStatusBadge = (status?: OptionStatus): ReactNode => {\n switch (status) {\n case OptionStatus.CORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"success\"\n iconLeft={<CheckCircleIcon aria-hidden=\"true\" />}\n >\n Resposta correta\n </Badge>\n );\n case OptionStatus.INCORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconLeft={<XCircleIcon aria-hidden=\"true\" />}\n >\n Resposta incorreta\n </Badge>\n );\n default:\n return null;\n }\n};\n\n/**\n * Container component for question content\n */\nexport const QuestionContainer = ({\n children,\n className,\n}: {\n children: ReactNode;\n className?: string;\n}) => {\n return (\n <div\n className={cn(\n 'bg-background rounded-t-xl px-4 pt-4 pb-[80px] h-auto flex flex-col gap-4 mb-auto',\n className\n )}\n >\n {children}\n </div>\n );\n};\n\n/**\n * Subtitle component for question sections\n */\nexport const QuestionSubTitle = ({ subTitle }: { subTitle: string }) => {\n return (\n <div className=\"px-4 pb-2 pt-6\">\n <Text size=\"md\" weight=\"bold\" color=\"text-text-950\">\n {subTitle}\n </Text>\n </div>\n );\n};\n\n/**\n * Internal component for fill in the blanks question\n * Uses useId hook to generate unique IDs\n *\n * Data structure:\n * - additionalContent: HTML text with {placeholderId} placeholders\n * - fillAnswers: Record<placeholderId, selectedOptionId> (what student chose)\n * - options: Array<{ id, option }> where id is placeholderId AND correct optionId\n * - Correctness: selectedOptionId === placeholderId means correct\n */\nexport const FillQuestionContent = ({\n question,\n result,\n}: QuestionRendererProps) => {\n const baseId = useId();\n\n // Get additionalContent (contains HTML with {placeholderId} placeholders)\n // Falls back to question.statement for older questions that may not have additionalContent\n const additionalContent =\n result?.additionalContent ||\n question.additionalContent ||\n question.statement ||\n '';\n\n // Strip HTML tags for clean text rendering\n const cleanText = stripHtmlTags(additionalContent);\n\n // Get options array (id is placeholderId, option is the text)\n const options =\n (\n result as {\n options?: Array<{ id: string; option: string; isCorrect: boolean }>;\n }\n )?.options ||\n question.options ||\n [];\n\n // Build a map of optionId -> option text for quick lookup\n const optionTextMap: Record<string, string> = {};\n options.forEach((opt) => {\n optionTextMap[opt.id] = opt.option;\n });\n\n // Get fillAnswers: Record<placeholderId, selectedOptionId>\n const fillAnswers: Record<string, string> = {};\n try {\n const resultWithFill = result as {\n fillAnswers?: Record<string, string> | null;\n };\n if (resultWithFill?.fillAnswers) {\n Object.assign(fillAnswers, resultWithFill.fillAnswers);\n } else if (result?.answer) {\n const parsed =\n typeof result.answer === 'string'\n ? JSON.parse(result.answer)\n : result.answer;\n if (typeof parsed === 'object') {\n Object.assign(fillAnswers, parsed);\n }\n }\n } catch {\n // Ignore parse errors\n }\n\n // Helper to get option text by ID - returns null if not found\n const getOptionTextById = (optionId: string): string | null => {\n if (!optionId) return null;\n return optionTextMap[optionId] ?? null;\n };\n\n // Check if answer is correct (selectedOptionId === placeholderId)\n const isAnswerCorrect = (placeholderId: string): boolean => {\n const selectedOptionId = fillAnswers[placeholderId];\n return selectedOptionId === placeholderId;\n };\n\n // Render student answer badge\n const renderStudentBadge = (placeholderId: string) => {\n const selectedOptionId = fillAnswers[placeholderId];\n const selectedOptionText = getOptionTextById(selectedOptionId);\n const isCorrect = isAnswerCorrect(placeholderId);\n\n if (!selectedOptionId) {\n return (\n <span className=\"inline-block align-middle mx-1 my-1\">\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconRight={<XCircleIcon aria-hidden=\"true\" />}\n size=\"large\"\n className=\"py-1 px-2\"\n >\n Não respondido\n </Badge>\n </span>\n );\n }\n\n // Handle case where option ID exists but text is not found\n const displayText = selectedOptionText ?? 'Opção não encontrada';\n\n return (\n <span className=\"inline-block align-middle mx-1 my-1\">\n <Badge\n variant=\"solid\"\n action={isCorrect ? 'success' : 'error'}\n iconRight={\n isCorrect ? (\n <CheckCircleIcon aria-hidden=\"true\" />\n ) : (\n <XCircleIcon aria-hidden=\"true\" />\n )\n }\n size=\"large\"\n className=\"py-1 px-2\"\n >\n {displayText}\n {/* O certo/errado só aparece pela cor e pelo ícone; o leitor de\n tela precisa do resultado em texto (WCAG 1.4.1). */}\n <Text as=\"span\" className=\"sr-only\">\n {isCorrect ? ' — correta' : ' — incorreta'}\n </Text>\n </Badge>\n </span>\n );\n };\n\n // Render correct answer\n const renderCorrectAnswer = (placeholderId: string) => {\n // The placeholderId IS the correct option ID\n const correctOptionText = getOptionTextById(placeholderId);\n const displayText = correctOptionText ?? '[Resposta não disponível]';\n return (\n <span className=\"inline mx-1 text-success-600 font-semibold border-b-2 border-success-600\">\n {/* O destaque verde não chega ao leitor de tela: o prefixo diz que\n este é o gabarito da lacuna. */}\n <Text as=\"span\" className=\"sr-only\">\n Resposta esperada:{' '}\n </Text>\n {displayText}\n </span>\n );\n };\n\n // Parse text and render with elements\n const renderTextWithElements = (text: string, isCorrectAnswer: boolean) => {\n const elements: Array<{ element: string | ReactNode; id: string }> = [];\n let lastIndex = 0;\n const nextId = () => elements.length;\n\n const regex = /\\{([a-zA-Z0-9-]+)\\}/g;\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n const [fullMatch, placeholderId] = match;\n const startIndex = match.index;\n\n // Add text before placeholder\n if (startIndex > lastIndex) {\n elements.push({\n element: text.slice(lastIndex, startIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n // Add the appropriate element\n if (isCorrectAnswer) {\n elements.push({\n element: renderCorrectAnswer(placeholderId),\n id: `${baseId}-answer-sheet-${nextId()}`,\n });\n } else {\n elements.push({\n element: renderStudentBadge(placeholderId),\n id: `${baseId}-student-${nextId()}`,\n });\n }\n\n lastIndex = match.index + fullMatch.length;\n }\n\n // Add remaining text\n if (lastIndex < text.length) {\n elements.push({\n element: text.slice(lastIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n return elements;\n };\n\n if (!additionalContent) {\n return (\n <div className=\"pt-2\">\n <Text size=\"md\" color=\"text-text-600\" weight=\"normal\">\n Nenhum conteúdo disponível para esta questão.\n </Text>\n </div>\n );\n }\n\n return (\n <div className=\"pt-2 space-y-4\">\n <div className=\"space-y-2\">\n <Text size=\"xs\" weight=\"normal\" color=\"text-text-500\">\n Resposta do aluno:\n </Text>\n <div className=\"p-3 bg-background-50 rounded-lg border border-border-100\">\n <Text\n as=\"div\"\n size=\"md\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderTextWithElements(cleanText, false).map((element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n ))}\n </Text>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Text size=\"xs\" weight=\"normal\" color=\"text-text-500\">\n Gabarito:\n </Text>\n <div className=\"p-3 bg-background-50 rounded-lg border border-border-100\">\n <Text\n as=\"div\"\n size=\"md\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderTextWithElements(cleanText, true).map((element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n ))}\n </Text>\n </div>\n </div>\n </div>\n );\n};\n","import { getStatusStyles } from '../Quiz/QuizContent';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport { getStatusBadge } from '../../utils/questionRenderer/components';\nimport { HtmlMathRenderer } from '../HtmlMathRenderer';\nimport { TrueFalseEnum } from '../../enums/Quiz';\nimport { OptionStatus } from '../../enums/Options';\n\n/** One statement of a true/false question, already normalized for rendering. */\nexport interface TrueFalseStatement {\n id: string;\n /** Statement text; may contain HTML and LaTeX */\n statement: string;\n /** What the student marked. Null when the statement was left blank. */\n studentMark: TrueFalseEnum | null;\n /** Answer key: whether the statement is true. Null when unknown. */\n isTrue: boolean | null;\n}\n\nexport interface TrueFalseStatementListProps {\n readonly statements: readonly TrueFalseStatement[];\n /**\n * Whether the answer key may be revealed. False while the answer is still\n * pending evaluation or was never submitted.\n */\n readonly showCorrectness?: boolean;\n}\n\n/**\n * Read-only list of true/false statements with the student's mark and the\n * answer key.\n *\n * Shared by the activity correction modal and the simulation detail modal so\n * both read the same way. The two callers reach this from different payload\n * shapes — `QuestionResult.selectedOptions` in one, an option's `selectedValue`\n * in the other — and normalize into {@link TrueFalseStatement} before calling.\n *\n * @param props - Component props\n * @returns JSX element\n *\n * @example\n * ```tsx\n * <TrueFalseStatementList\n * statements={[{ id: '1', statement: 'A água ferve a 100°C', studentMark: TrueFalseEnum.VERDADEIRO, isTrue: true }]}\n * showCorrectness\n * />\n * ```\n */\nexport const TrueFalseStatementList = ({\n statements,\n showCorrectness = true,\n}: TrueFalseStatementListProps) => {\n // 97 = 'a' in ASCII\n const getLetterByIndex = (index: number) => String.fromCodePoint(97 + index);\n\n return (\n <div className=\"pt-2\">\n <div className=\"flex flex-col gap-3.5\">\n {statements.map((item, index) => {\n const hasAnswerKey = item.isTrue !== null;\n const hasAnswered = item.studentMark !== null;\n const studentMarkedTrue =\n item.studentMark === TrueFalseEnum.VERDADEIRO;\n\n const isStudentCorrect =\n hasAnswerKey && hasAnswered && studentMarkedTrue === item.isTrue;\n\n // Correctness is only shown once the answer key is known AND the\n // answer is no longer pending.\n const canShowCorrectness = showCorrectness && hasAnswerKey;\n const variantCorrect = isStudentCorrect\n ? OptionStatus.CORRECT\n : OptionStatus.INCORRECT;\n const correctAnswer = item.isTrue\n ? TrueFalseEnum.VERDADEIRO\n : TrueFalseEnum.FALSO;\n\n return (\n <section\n key={item.id || `statement-${index}`}\n className=\"flex flex-col gap-2\"\n >\n <div\n className={cn(\n 'flex flex-row justify-between items-center gap-2 p-2 rounded-md border',\n canShowCorrectness && hasAnswered\n ? getStatusStyles(variantCorrect)\n : ''\n )}\n >\n <Text as=\"span\" size=\"sm\" weight=\"normal\" color=\"text-text-900\">\n {getLetterByIndex(index).concat(') ')}\n <HtmlMathRenderer content={item.statement} inline />\n </Text>\n\n {canShowCorrectness && hasAnswered && (\n <div className=\"flex-shrink-0\">\n {getStatusBadge(variantCorrect)}\n </div>\n )}\n </div>\n\n {canShowCorrectness && (\n <span className=\"flex flex-row gap-2 items-center flex-wrap\">\n {hasAnswered ? (\n <>\n <Text size=\"2xs\" weight=\"normal\" color=\"text-text-800\">\n Resposta selecionada: {item.studentMark}\n </Text>\n {!isStudentCorrect && (\n <Text size=\"2xs\" weight=\"normal\" color=\"text-text-800\">\n | Resposta correta: {correctAnswer}\n </Text>\n )}\n </>\n ) : (\n <Text size=\"2xs\" weight=\"normal\" color=\"text-text-800\">\n Não respondida | Resposta correta: {correctAnswer}\n </Text>\n )}\n </span>\n )}\n </section>\n );\n })}\n </div>\n </div>\n );\n};\n\nexport default TrueFalseStatementList;\n","/**\n * Question status enum for student activity correction\n * Maps from ANSWER_STATUS to a simpler status for UI display\n */\nexport const QUESTION_STATUS = {\n CORRETA: 'CORRETA',\n INCORRETA: 'INCORRETA',\n EM_BRANCO: 'EM_BRANCO',\n /** Reserved for future use - pending teacher evaluation for essay questions */\n PENDENTE: 'PENDENTE',\n} as const;\n\nexport type QuestionStatus =\n (typeof QUESTION_STATUS)[keyof typeof QUESTION_STATUS];\n","import { StatusBadgeConfig } from '@/types/activityDetails';\nimport {\n ANSWER_STATUS,\n QUESTION_TYPE,\n} from '../../components/Quiz/useQuizStore';\nimport { QUESTION_STATUS, type QuestionStatus } from './constants';\nimport type { CorrectionQuestionData } from './types';\n\n/**\n * Returns whether the answer is correct, incorrect, or null based on the answer status.\n * @param answerStatus - Answer status from ANSWER_STATUS enum\n * @returns Returns true for correct, false for incorrect, or null if undefined.\n */\nexport const getIsCorrect = (answerStatus: ANSWER_STATUS): boolean | null => {\n if (answerStatus === ANSWER_STATUS.RESPOSTA_CORRETA) return true;\n if (answerStatus === ANSWER_STATUS.RESPOSTA_INCORRETA) return false;\n return null;\n};\n\n/**\n * Map ANSWER_STATUS from Quiz to QUESTION_STATUS for correction modal\n * @param answerStatus - Answer status from QuestionResult\n * @returns QuestionStatus for UI display\n */\nexport const mapAnswerStatusToQuestionStatus = (\n answerStatus: ANSWER_STATUS\n): QuestionStatus => {\n switch (answerStatus) {\n case ANSWER_STATUS.RESPOSTA_CORRETA:\n return QUESTION_STATUS.CORRETA;\n case ANSWER_STATUS.RESPOSTA_INCORRETA:\n return QUESTION_STATUS.INCORRETA;\n case ANSWER_STATUS.NAO_RESPONDIDO:\n return QUESTION_STATUS.EM_BRANCO;\n case ANSWER_STATUS.PENDENTE_AVALIACAO:\n return QUESTION_STATUS.PENDENTE;\n default:\n return QUESTION_STATUS.EM_BRANCO;\n }\n};\n\n/**\n * Get question status badge configuration\n * @param status - Question status\n * @returns Badge configuration with label and colors\n */\nexport const getQuestionStatusBadgeConfig = (status: QuestionStatus) => {\n const configs: Partial<Record<QuestionStatus, StatusBadgeConfig>> = {\n [QUESTION_STATUS.CORRETA]: {\n label: 'Correta',\n bgColor: 'bg-success-background',\n textColor: 'text-success-800',\n },\n [QUESTION_STATUS.INCORRETA]: {\n label: 'Incorreta',\n bgColor: 'bg-error-background',\n textColor: 'text-error-800',\n },\n [QUESTION_STATUS.EM_BRANCO]: {\n label: 'Em branco',\n bgColor: 'bg-gray-100',\n textColor: 'text-gray-600',\n },\n [QUESTION_STATUS.PENDENTE]: {\n label: 'Pendente',\n bgColor: 'bg-warning-background',\n textColor: 'text-warning-800',\n },\n };\n\n const defaultConfig = {\n label: 'Sem categoria',\n bgColor: 'bg-gray-100',\n textColor: 'text-gray-600',\n };\n\n return configs[status] ?? defaultConfig;\n};\n\n/**\n * Check if question can be auto-validated based on options\n * All question types can be auto-validated except DISSERTATIVA (essay questions)\n * @param questionData - Correction question data\n * @returns true if can auto-validate, false otherwise\n */\nexport const canAutoValidate = (\n questionData: CorrectionQuestionData\n): boolean => {\n const { result } = questionData;\n\n // Only dissertative questions require manual evaluation\n if (result.questionType === QUESTION_TYPE.DISSERTATIVA) {\n return false;\n }\n\n return true;\n};\n\ntype Option = {\n id: string;\n option: string;\n isCorrect: boolean;\n};\n\n/**\n * Type predicate to check if an option has isCorrect defined\n */\nconst hasIsCorrect = (op: {\n id: string;\n option: string;\n isCorrect?: boolean | null;\n}): op is Option => {\n return op.isCorrect != null;\n};\n\n/**\n * Validate alternativa (single choice) question\n * Must select exactly one correct option\n */\nconst validateAlternativa = (\n selected: Set<string>,\n options: Option[]\n): ANSWER_STATUS => {\n if (selected.size !== 1) return ANSWER_STATUS.RESPOSTA_INCORRETA;\n\n const [selectedId] = selected;\n return options.find((o) => o.id === selectedId)?.isCorrect\n ? ANSWER_STATUS.RESPOSTA_CORRETA\n : ANSWER_STATUS.RESPOSTA_INCORRETA;\n};\n\n/**\n * Validate multipla escolha (multiple choice) question\n * Each option must match its correct state: selected if correct, not selected if incorrect\n */\nconst validateMultiplaEscolha = (\n selected: Set<string>,\n options: Option[]\n): ANSWER_STATUS => {\n const allMatch = options.every((op) => selected.has(op.id) === op.isCorrect);\n\n return allMatch\n ? ANSWER_STATUS.RESPOSTA_CORRETA\n : ANSWER_STATUS.RESPOSTA_INCORRETA;\n};\n\n/**\n * Validate verdadeiro/falso (true/false) question\n * Each statement is evaluated individually: if statement is true, must be selected;\n * if statement is false, must not be selected\n */\nconst validateVerdadeiroFalso = validateMultiplaEscolha;\n\nconst validators: Partial<\n Record<QUESTION_TYPE, (s: Set<string>, o: Option[]) => ANSWER_STATUS>\n> = {\n [QUESTION_TYPE.ALTERNATIVA]: validateAlternativa,\n [QUESTION_TYPE.MULTIPLA_ESCOLHA]: validateMultiplaEscolha,\n [QUESTION_TYPE.VERDADEIRO_FALSO]: validateVerdadeiroFalso,\n};\n\n/**\n * Auto-validate question based on selected options vs correct options\n * @param questionData - Correction question data\n * @returns ANSWER_STATUS if can determine, null otherwise\n */\nexport const autoValidateQuestion = (\n questionData: CorrectionQuestionData\n): ANSWER_STATUS | null => {\n const { result } = questionData;\n\n if (!canAutoValidate(questionData) || !result.options) return null;\n\n // Filter options to only include those with isCorrect defined\n const validOptions = result.options.filter(hasIsCorrect);\n if (validOptions.length === 0) return null;\n\n const selected = new Set(\n result.selectedOptions?.map((o) => o.optionId) ?? []\n );\n\n if (selected.size === 0) return ANSWER_STATUS.NAO_RESPONDIDO;\n\n const validator = validators[result.questionType];\n if (!validator) return null;\n\n return validator(selected, validOptions);\n};\n\n/**\n * Get question status from CorrectionQuestionData\n * Maps the result's answerStatus to QuestionStatus\n * If status is PENDENTE_AVALIACAO but can auto-validate, calculates status automatically\n * @param questionData - Correction question data\n * @returns QuestionStatus for UI display\n */\nexport const getQuestionStatusFromData = (\n questionData: CorrectionQuestionData\n): QuestionStatus => {\n const { result } = questionData;\n\n // If pending evaluation, try to auto-validate\n if (\n result.answerStatus === ANSWER_STATUS.PENDENTE_AVALIACAO &&\n canAutoValidate(questionData)\n ) {\n const autoValidatedStatus = autoValidateQuestion(questionData);\n if (autoValidatedStatus !== null) {\n return mapAnswerStatusToQuestionStatus(autoValidatedStatus);\n }\n }\n\n return mapAnswerStatusToQuestionStatus(result.answerStatus);\n};\n","import type { SimulationContentSummary } from '../../components/shared/SimulationSummaryCards';\nimport { QUESTION_STATUS } from './constants';\nimport type { CorrectionQuestionData } from './types';\nimport { getQuestionStatusFromData } from './utils';\n\n/**\n * Hit rate above which a lone subtema reads as a result rather than a gap.\n *\n * The same 50% the report's `findBestAndWorstTopics` uses, so a student's\n * correction modal and their row in the Desempenho report never disagree on\n * which side of the line one subtema falls.\n */\nconst PERFORMANCE_THRESHOLD = 50;\n\n/** Running tally of one subtema across the student's answers. */\ninterface SubtopicTally {\n name: string;\n answered: number;\n correct: number;\n}\n\n/**\n * The subtema a question belongs to, or null when it hangs outside the\n * knowledge matrix (the API sends `{ id: '', name: '' }` for those).\n */\nfunction getSubtopic(\n questionData: CorrectionQuestionData\n): { id: string; name: string } | null {\n const subtopic = questionData.question.knowledgeMatrix?.[0]?.subtopic;\n return subtopic?.id && subtopic.name ? subtopic : null;\n}\n\n/**\n * Best and worst subtema of one student's answers to an activity.\n *\n * The correction endpoint answers question by question, so the two cards are\n * derived here rather than sent: answers are grouped by the subtema of their\n * knowledge matrix and ranked by hit rate.\n *\n * Only answered questions count. A blank or still-ungraded question says\n * nothing about the subtema, and counting it as a miss would name a \"maior\n * dificuldade\" the student never attempted.\n *\n * @param questions - The student's questions, as the modal lists them\n * @returns The best and the worst subtema; either is null when there is none\n *\n * @example\n * ```typescript\n * findBestAndWorstSubtopics(questions);\n * // { best: { contentName: 'Cinemática' }, worst: { contentName: 'Óptica' } }\n * ```\n */\nexport function findBestAndWorstSubtopics(\n questions: readonly CorrectionQuestionData[]\n): {\n best: SimulationContentSummary | null;\n worst: SimulationContentSummary | null;\n} {\n const tallies = new Map<string, SubtopicTally>();\n\n for (const questionData of questions) {\n const subtopic = getSubtopic(questionData);\n if (!subtopic) continue;\n\n const status = getQuestionStatusFromData(questionData);\n if (\n status !== QUESTION_STATUS.CORRETA &&\n status !== QUESTION_STATUS.INCORRETA\n ) {\n continue;\n }\n\n const tally = tallies.get(subtopic.id) ?? {\n name: subtopic.name,\n answered: 0,\n correct: 0,\n };\n tally.answered += 1;\n if (status === QUESTION_STATUS.CORRETA) tally.correct += 1;\n tallies.set(subtopic.id, tally);\n }\n\n // Ties keep the order the questions came in, which is the order of the\n // activity — `sort` is stable, so no extra key is needed for that.\n const ranked = [...tallies.values()]\n .map((tally) => ({\n contentName: tally.name,\n rate: (tally.correct / tally.answered) * 100,\n }))\n .sort((a, b) => b.rate - a.rate);\n\n if (ranked.length === 0) return { best: null, worst: null };\n\n if (ranked.length === 1) {\n const [only] = ranked;\n const summary = { contentName: only.contentName };\n return only.rate >= PERFORMANCE_THRESHOLD\n ? { best: summary, worst: null }\n : { best: null, worst: summary };\n }\n\n // The list is sorted by hit rate and has at least two entries by this point,\n // so the ends of it are the two cards.\n const [best] = ranked;\n const [worst] = ranked.slice(-1);\n\n return {\n best: { contentName: best.contentName },\n worst: { contentName: worst.contentName },\n };\n}\n","import type { SimulatedPerformanceTag } from '../../components/SimulatedStudentSimulationsModal/types';\n\n/**\n * Percentage floors of each band, as the reports classify a student.\n *\n * The absolute scale (`performanceMode=absolute`), not one relative to the\n * class: this modal describes a single student's activity, so there is no\n * cohort to compare them against.\n */\nconst BAND_FLOORS = {\n HIGHLIGHT: 90,\n ABOVE_AVERAGE: 70,\n BELOW_AVERAGE: 40,\n} as const;\n\n/**\n * The band the student's grade falls in, for the badge of the correction modal.\n *\n * The correction endpoint sends the stored grade (0-10) and no tag, so the\n * badge is derived here with the bands the Desempenho report uses, on the\n * percentage scale those bands are written in.\n *\n * @param score - Grade out of ten; null while the activity is ungraded\n * @returns The band, or null when there is no grade to classify\n *\n * @example\n * ```typescript\n * getCorrectionPerformanceTag(9.2); // 'HIGHLIGHT'\n * getCorrectionPerformanceTag(null); // null\n * ```\n */\nexport function getCorrectionPerformanceTag(\n score: number | null | undefined\n): SimulatedPerformanceTag | null {\n if (score === null || score === undefined || !Number.isFinite(score)) {\n return null;\n }\n\n const percentage = score * 10;\n if (percentage >= BAND_FLOORS.HIGHLIGHT) return 'HIGHLIGHT';\n if (percentage >= BAND_FLOORS.ABOVE_AVERAGE) return 'ABOVE_AVERAGE';\n if (percentage >= BAND_FLOORS.BELOW_AVERAGE) return 'BELOW_AVERAGE';\n return 'ATTENTION_POINT';\n}\n","import type { Question } from '../../components/Quiz/useQuizStore';\nimport { QUESTION_TYPE } from '../../components/Quiz/useQuizStore';\nimport type {\n QuestionsAnswersByStudentResponse,\n StudentActivityCorrectionData,\n CorrectionQuestionData,\n QuestionCorrection,\n} from './types';\nimport { getIsCorrect } from './utils';\n\n/**\n * Build Question object from answer data\n * @param answer - Answer data from QuestionResult\n * @returns Question object in Quiz format\n */\nconst buildQuestionFromAnswer = (\n answer: QuestionsAnswersByStudentResponse['data']['answers'][number]\n): Question => {\n return {\n id: answer.questionId,\n statement: answer.statement,\n questionType: answer.questionType,\n difficultyLevel: answer.difficultyLevel,\n description: '',\n examBoard: null,\n examYear: null,\n solutionExplanation: answer.solutionExplanation || null,\n additionalContent: answer.additionalContent || null,\n answer: null,\n answerStatus: answer.answerStatus,\n options: answer.options || [],\n knowledgeMatrix: answer.knowledgeMatrix.map((matrix) => ({\n areaKnowledge: matrix.areaKnowledge || {\n id: '',\n name: '',\n },\n subject: matrix.subject || {\n id: '',\n name: '',\n color: '',\n icon: '',\n },\n topic: matrix.topic || {\n id: '',\n name: '',\n },\n subtopic: matrix.subtopic || {\n id: '',\n name: '',\n },\n content: matrix.content || {\n id: '',\n name: '',\n },\n })),\n correctOptionIds:\n answer.options?.filter((opt) => opt.isCorrect).map((opt) => opt.id) || [],\n };\n};\n\n/**\n * Build correction data for a question\n *\n * Built for every question type, not just DISSERTATIVA: a teacher can comment on\n * any question. `isCorrect` stays null outside DISSERTATIVA because objective\n * questions are graded automatically and must not be re-graded by hand.\n *\n * The AI fields are carried through untouched rather than folded into\n * `teacherFeedback`. The modal needs both to do its job: the AI's text to\n * prefill the field the teacher edits, and the source to decide which tag to\n * show and whether saving turns an `IA` correction into an `IA + PROFESSOR` one.\n *\n * @param answer - Answer data from QuestionResult\n * @returns QuestionCorrection with the teacher's grade and/or comment\n */\nconst buildQuestionCorrection = (\n answer: QuestionsAnswersByStudentResponse['data']['answers'][number]\n): QuestionCorrection => ({\n isCorrect:\n answer.questionType === QUESTION_TYPE.DISSERTATIVA\n ? getIsCorrect(answer.answerStatus)\n : null,\n teacherFeedback: answer.teacherFeedback || '',\n aiFeedback: answer.aiFeedback ?? null,\n aiIsCorrect: answer.aiIsCorrect ?? null,\n aiCorrectionStatus: answer.aiCorrectionStatus ?? null,\n correctionSource: answer.correctionSource ?? null,\n});\n\n/**\n * Convert a single answer to CorrectionQuestionData format\n * @param answer - Answer data from QuestionResult\n * @param index - Index of the answer (0-based)\n * @returns CorrectionQuestionData formatted for the modal\n */\nconst convertAnswerToQuestionData = (\n answer: QuestionsAnswersByStudentResponse['data']['answers'][number],\n index: number\n): CorrectionQuestionData => {\n const question = buildQuestionFromAnswer(answer);\n const correction = buildQuestionCorrection(answer);\n\n return {\n question,\n result: answer,\n questionNumber: index + 1,\n correction,\n };\n};\n\n/**\n * Convert QuestionResult from API to StudentActivityCorrectionData\n * This function transforms the API response into the format expected by CorrectActivityModal\n *\n * `statistics.score` from the answers endpoint is a 0-100 percentage recomputed\n * from the per-question rows, while the students table shows the stored 0-10\n * grade. Callers that already hold the stored grade pass it as `score` so the\n * modal and the table never disagree; `null` is an explicit \"no grade yet\".\n *\n * @param apiResponse - API response with answers and statistics\n * @param studentId - Student ID\n * @param studentName - Student name\n * @param observation - Optional teacher observation\n * @param attachment - Optional attachment URL\n * @param score - Optional stored grade (0-10) that takes precedence over\n * `statistics.score`; `null` means the student has no grade yet\n * @returns StudentActivityCorrectionData formatted for the modal\n */\nexport const convertApiResponseToCorrectionData = (\n apiResponse: QuestionsAnswersByStudentResponse,\n studentId: string,\n studentName: string,\n observation?: string,\n attachment?: string,\n score?: number | null\n): StudentActivityCorrectionData => {\n const { answers, statistics } = apiResponse.data;\n\n // Calculate counts from statistics\n const correctCount = statistics.correctAnswers;\n const incorrectCount = statistics.incorrectAnswers;\n const blankCount = statistics.totalAnswered - correctCount - incorrectCount;\n\n // Convert answers to CorrectionQuestionData format\n const questions: CorrectionQuestionData[] = answers.map((answer, index) =>\n convertAnswerToQuestionData(answer, index)\n );\n\n return {\n studentId,\n studentName,\n score: score === undefined ? (statistics.score ?? null) : score,\n correctCount,\n incorrectCount,\n blankCount,\n questions,\n observation,\n attachment,\n };\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport type { ChangeEvent } from 'react';\nimport { PaperclipIcon } from '@phosphor-icons/react/dist/csr/Paperclip';\nimport { XIcon } from '@phosphor-icons/react/dist/csr/X';\nimport Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport TextArea from '../TextArea/TextArea';\nimport { CardAccordation } from '../Accordation';\nimport { SkeletonCard } from '../Skeleton/Skeleton';\nimport { QuestionCommentField } from '../shared/QuestionCommentField';\nimport {\n TrueFalseStatementList,\n type TrueFalseStatement,\n} from '../shared/TrueFalseStatementList';\nimport { ImageAnswerView } from '../shared/ImageAnswerView';\nimport { DEFAULT_IMAGE_TOLERANCE } from '../../utils/image/imageAnswer.utils';\nimport { AlternativesList } from '../Alternative/Alternative';\nimport { HtmlMathRenderer } from '../HtmlMathRenderer';\nimport { OptionStatus } from '../../enums/Options';\nimport { QUESTION_TYPE } from '../Quiz/useQuizStore';\nimport {\n getQuestionStatusBadgeConfig,\n QUESTION_STATUS,\n type QuestionStatus,\n} from '../../utils/studentActivityCorrection';\nimport {\n buildSimulationMeta,\n ContentCards,\n DataCard,\n SectionTitle,\n SimulationCardShell,\n SimulationStatCards,\n StudentSummaryHeader,\n} from '../shared/SimulationSummaryCards';\nimport { cn } from '../../utils/utils';\nimport { formatQuestionDuration } from '../../utils/questionDuration';\nimport { formatTimeSpent } from '../../utils/activityDetailsUtils';\nimport type { BaseApiClient } from '../../types/api';\nimport { createUseSimulations } from '../../hooks/useSimulations';\nimport {\n useSimulationCardDetails,\n type SimulationCardHandlers,\n type SimulationDetailState,\n type SimulationNoteState,\n} from '../../hooks/useSimulationCardDetails';\nimport type {\n SimulationsListData,\n SimulationDetailQuestion,\n StudentSimulationItem,\n NoteData,\n} from '../../types/simulations';\n\nexport interface SimulationsDetailModalProps {\n /** API client used to fetch the student's simulations */\n readonly api: BaseApiClient;\n readonly isOpen: boolean;\n readonly onClose: () => void;\n /** The student whose simulations are shown (null closes the modal) */\n readonly student: { userInstitutionId: string; name: string } | null;\n}\n\n/** Map the simulation question status to the shared correction status. */\nconst QUESTION_STATUS_MAP: Record<\n SimulationDetailQuestion['status'],\n QuestionStatus\n> = {\n CORRECT: QUESTION_STATUS.CORRETA,\n INCORRECT: QUESTION_STATUS.INCORRETA,\n BLANK: QUESTION_STATUS.EM_BRANCO,\n PENDING: QUESTION_STATUS.PENDENTE,\n};\n\n/** Label of the inner accordion holding the student's answer. */\nfunction getAnswerAccordionTitle(questionType: string): string {\n if (questionType === QUESTION_TYPE.DISSERTATIVA) {\n return 'Resposta do aluno';\n }\n if (questionType === QUESTION_TYPE.VERDADEIRO_FALSO) {\n return 'Afirmações';\n }\n if (questionType === QUESTION_TYPE.IMAGEM) {\n return 'Imagem';\n }\n return 'Alternativas';\n}\n\n// ---------------------------------------------------------------------------\n// Level 2 — Question (reuses the shared alternatives renderer + status badge)\n// ---------------------------------------------------------------------------\n\nexport interface SimulationQuestionItemProps {\n readonly question: SimulationDetailQuestion;\n /** Zero-based position in the simulation; shown as \"Questão N\". */\n readonly index: number;\n /**\n * Persist the teacher comment on this question; an empty string clears it.\n * Left out, the question is read-only and has no comment field — a report\n * that shows the answers but has nobody to write to (the Momento ENEM\n * report of the gestor app).\n */\n readonly onSaveComment?: (comment: string) => Promise<void>;\n}\n\n/**\n * One question of a student's simulation: an accordion with the status badge\n * in its header and, inside, the statement, the student's answer (alternatives,\n * true/false marks, essay text or image click, by question type) and, when\n * the caller can save it, the teacher comment field.\n *\n * Exported so consumers that list a student's simulations in their own layout\n * (the Desempenho report of the teacher app, the Momento ENEM report of the\n * gestor app) render questions exactly as the Simulados page does.\n */\nexport function SimulationQuestionItem({\n question,\n index,\n onSaveComment,\n}: SimulationQuestionItemProps) {\n const badge = getQuestionStatusBadgeConfig(\n QUESTION_STATUS_MAP[question.status]\n );\n\n // The subject only joins the label when the question has one mapped in the\n // knowledge matrix; without it the row falls back to a plain \"Questão N\".\n const label = [`Questão ${index + 1}`, question.subject]\n .filter(Boolean)\n .join(' - ');\n\n // Time spent sits on the right, next to the badge, and not glued to the\n // label: it is a measurement, read down the column against the other\n // questions, not part of the question's name. Null when it was never\n // measured, and then the row shows nothing rather than `00:00:00`.\n const duration = formatQuestionDuration(question.timeSpent);\n\n const alternatives = question.options.map((option) => {\n let status: OptionStatus;\n if (option.isCorrect) {\n status = OptionStatus.CORRECT;\n } else if (option.isSelected) {\n status = OptionStatus.INCORRECT;\n } else {\n status = OptionStatus.NEUTRAL;\n }\n return { label: option.option, value: option.id, status };\n });\n\n // An essay has no alternatives to show — it has the text the student wrote.\n // Rendering the \"Alternativas\" accordion for it produced an empty box and hid\n // the answer entirely, leaving the teacher to comment on nothing.\n const isEssay = question.questionType === QUESTION_TYPE.DISSERTATIVA;\n\n // True/false never writes `option_id`, so `isSelected` is always false and the\n // student's marks live in each option's `selectedValue`. Sending it through\n // the alternatives branch showed the answer key as if it were the student's\n // answer, and no mark at all.\n const isTrueFalse = question.questionType === QUESTION_TYPE.VERDADEIRO_FALSO;\n const trueFalseStatements: TrueFalseStatement[] = question.options.map(\n (option) => ({\n id: option.id,\n statement: option.option,\n studentMark: option.selectedValue ?? null,\n isTrue: option.isCorrect,\n })\n );\n\n // IMAGEM has no alternatives either: the answer is a point on an image. The\n // alternatives branch rendered the answer key's raw JSON as an option label,\n // painted green, and never showed where the student clicked.\n const isImage = question.questionType === QUESTION_TYPE.IMAGEM;\n\n /**\n * Render the student's answer, shaped by the question type.\n */\n const renderAnswerArea = () => {\n if (isImage) {\n return (\n <ImageAnswerView\n imageUrl={question.additionalContent ?? ''}\n correctPoint={question.correctPoint}\n studentPoint={\n question.imageAnswer\n ? {\n x: question.imageAnswer.coordinateX,\n y: question.imageAnswer.coordinateY,\n }\n : null\n }\n toleranceRadius={question.imageTolerance ?? DEFAULT_IMAGE_TOLERANCE}\n />\n );\n }\n\n if (isEssay) {\n return (\n <div className=\"rounded-lg border border-border-100 bg-background-50 p-3\">\n {question.answer ? (\n <HtmlMathRenderer\n content={question.answer}\n className=\"text-sm text-text-800\"\n />\n ) : (\n <Text size=\"sm\" className=\"text-text-600\">\n Nenhuma resposta fornecida\n </Text>\n )}\n </div>\n );\n }\n\n if (isTrueFalse) {\n return <TrueFalseStatementList statements={trueFalseStatements} />;\n }\n\n return (\n <AlternativesList\n mode=\"readonly\"\n layout=\"compact\"\n name={`question-${question.questionId}`}\n alternatives={alternatives}\n selectedValue={question.selectedOptionId ?? ''}\n />\n );\n };\n\n return (\n <CardAccordation\n value={question.questionId}\n trigger={\n <div className=\"flex flex-1 items-center justify-between gap-3 py-3\">\n <Text size=\"sm\" weight=\"bold\" className=\"min-w-0 text-text-950\">\n {label}\n </Text>\n <div className=\"flex shrink-0 items-center gap-2\">\n {duration && (\n <Text\n size=\"xs\"\n className=\"whitespace-nowrap tabular-nums text-text-600\"\n >\n {duration}\n </Text>\n )}\n <span\n className={cn(\n 'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium',\n badge.bgColor,\n badge.textColor\n )}\n >\n {badge.label}\n </span>\n </div>\n </div>\n }\n contentClassName=\"px-3 pb-3\"\n >\n <div className=\"flex flex-col gap-3\">\n <HtmlMathRenderer\n content={question.statement}\n className=\"text-sm text-text-800\"\n />\n <CardAccordation\n value={`${question.questionId}-answer`}\n trigger={\n <div className=\"flex-1 py-2\">\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-950\">\n {getAnswerAccordionTitle(question.questionType)}\n </Text>\n </div>\n }\n contentClassName=\"px-3 pb-3\"\n >\n {renderAnswerArea()}\n </CardAccordation>\n {onSaveComment && (\n <QuestionCommentField\n value={question.teacherComment ?? ''}\n onSave={onSaveComment}\n />\n )}\n </div>\n </CardAccordation>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Note (\"Observação\")\n// ---------------------------------------------------------------------------\n\nexport interface SimulationNoteRowProps {\n /** Current observation, null when the teacher never wrote one. */\n readonly note: NoteData | null;\n readonly loading: boolean;\n /**\n * Message of a failed load. While it is set the row offers a retry instead\n * of the editor: writing over a note that could not be read would replace\n * it, and this save has no version to protect it.\n */\n readonly loadError?: string | null;\n /** Load the observation again; required whenever `loadError` can be set. */\n readonly onRetry?: () => void;\n /** Placeholder of the editor; defaults to the simulado wording. */\n readonly placeholder?: string;\n /**\n * Persist the observation. `text` is already trimmed and non-empty; `file`\n * is a newly chosen attachment still to be uploaded (null when none), and\n * `existingAttachment` is the URL of the saved file the teacher kept — null\n * when there was none, when it was removed, or when a new file replaces it.\n */\n readonly onSave: (\n text: string,\n file: File | null,\n existingAttachment: string | null\n ) => Promise<void>;\n}\n\n/**\n * Human label of an attachment URL: its file name, or a generic word when the\n * URL carries none.\n */\nfunction getAttachmentLabel(url: string): string {\n const lastSegment = url.split('?')[0].split('/').pop() ?? '';\n try {\n return decodeURIComponent(lastSegment) || 'Anexo';\n } catch {\n return lastSegment || 'Anexo';\n }\n}\n\n/**\n * Grey pill naming an attached file. Links to the file when `href` is given\n * and offers a remove button when `onRemove` is given — the same chip the\n * activity correction modal uses for its observation attachment.\n */\nfunction AttachmentChip({\n label,\n href,\n onRemove,\n}: {\n readonly label: string;\n readonly href?: string;\n readonly onRemove?: () => void;\n}) {\n const content = (\n <>\n <PaperclipIcon size={18} className=\"shrink-0 text-text-800\" />\n <span className=\"truncate text-md font-medium text-text-800\">\n {label}\n </span>\n </>\n );\n\n return (\n <div className=\"flex h-10 min-w-0 max-w-[220px] items-center gap-2 rounded-full bg-secondary-500 px-5\">\n {href ? (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex min-w-0 items-center gap-2 hover:underline\"\n >\n {content}\n </a>\n ) : (\n content\n )}\n {onRemove && (\n <button\n type=\"button\"\n onClick={onRemove}\n className=\"shrink-0 text-text-700 hover:text-text-950\"\n aria-label={`Remover ${label}`}\n >\n <XIcon size={18} />\n </button>\n )}\n </div>\n );\n}\n\n/**\n * The simulation-wide teacher observation: a row with the saved text, the\n * attached file (if any) and an \"Incluir\"/\"Editar\" button that swaps into a\n * textarea with \"Anexar\" and \"Salvar\" — the same flow as the observation of\n * the activity correction modal.\n *\n * Exported for the same reason as {@link SimulationQuestionItem}.\n */\nexport function SimulationNoteRow({\n note,\n loading,\n loadError,\n onRetry,\n placeholder = 'Escreva uma observação para este simulado',\n onSave,\n}: SimulationNoteRowProps) {\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState('');\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n /** File chosen in this editing session, not uploaded yet. */\n const [pendingFile, setPendingFile] = useState<File | null>(null);\n /** Saved attachment the teacher is keeping; null once removed. */\n const [keptAttachment, setKeptAttachment] = useState<string | null>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const savedAttachment = note?.attachment ?? null;\n\n const startEditing = () => {\n setDraft(note?.note ?? '');\n setPendingFile(null);\n setKeptAttachment(savedAttachment);\n setError(null);\n setEditing(true);\n };\n\n const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0] ?? null;\n if (file) setPendingFile(file);\n // Reset so the same file can be picked again after being removed.\n event.target.value = '';\n };\n\n const handleSave = async () => {\n if (!draft.trim()) return;\n setSaving(true);\n setError(null);\n try {\n await onSave(\n draft.trim(),\n pendingFile,\n pendingFile ? null : keptAttachment\n );\n setEditing(false);\n } catch {\n // Keep the editing UI open (draft preserved) and surface the failure.\n setError('Erro ao salvar a observação. Tente novamente.');\n } finally {\n setSaving(false);\n }\n };\n\n /** Left side of the footer: the chosen/kept file, or the \"Anexar\" button. */\n const renderAttachmentControl = () => {\n if (pendingFile) {\n return (\n <AttachmentChip\n label={pendingFile.name}\n onRemove={() => setPendingFile(null)}\n />\n );\n }\n if (keptAttachment) {\n return (\n <AttachmentChip\n label={getAttachmentLabel(keptAttachment)}\n href={keptAttachment}\n onRemove={() => setKeptAttachment(null)}\n />\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"medium\"\n onClick={() => fileInputRef.current?.click()}\n className=\"flex items-center gap-2\"\n >\n <PaperclipIcon size={18} />\n Anexar\n </Button>\n );\n };\n\n if (loading) {\n // O `SkeletonCard` já é `role=\"status\"` e anuncia o carregamento.\n return <SkeletonCard className=\"h-14\" />;\n }\n\n if (loadError) {\n return (\n <div className=\"flex items-center justify-between gap-2 rounded-lg border border-border-200 bg-background p-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <Text size=\"md\" weight=\"bold\" className=\"text-text-950\">\n Observação\n </Text>\n {/* `role=\"alert\"`: a falha precisa ser anunciada na hora. */}\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {loadError}\n </Text>\n </div>\n {onRetry && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"medium\"\n onClick={onRetry}\n className=\"shrink-0\"\n >\n Tentar novamente\n </Button>\n )}\n </div>\n );\n }\n\n if (editing) {\n return (\n <div className=\"flex flex-col gap-4 rounded-lg border border-border-200 bg-background p-4\">\n <Text size=\"md\" weight=\"bold\" className=\"text-text-950\">\n Observação\n </Text>\n <TextArea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder={placeholder}\n rows={3}\n />\n {error && (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {error}\n </Text>\n )}\n {/* Only images: the pre-signed upload endpoint accepts nothing else. */}\n <input\n type=\"file\"\n ref={fileInputRef}\n className=\"hidden\"\n accept=\"image/*\"\n onChange={handleFileChange}\n aria-label=\"Selecionar arquivo\"\n />\n <div className=\"flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between\">\n {renderAttachmentControl()}\n <Button\n type=\"button\"\n variant=\"solid\"\n size=\"medium\"\n onClick={handleSave}\n disabled={saving || !draft.trim()}\n >\n Salvar\n </Button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-center justify-between gap-2 rounded-lg border border-border-200 bg-background p-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <Text size=\"md\" weight=\"bold\" className=\"text-text-950\">\n Observação\n </Text>\n {note?.note && (\n <Text size=\"sm\" className=\"truncate text-text-700\">\n {note.note}\n </Text>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {savedAttachment && (\n <AttachmentChip\n label={getAttachmentLabel(savedAttachment)}\n href={savedAttachment}\n />\n )}\n <Button\n type=\"button\"\n variant=\"solid\"\n size=\"medium\"\n onClick={startEditing}\n >\n {note?.note ? 'Editar' : 'Incluir'}\n </Button>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Level 1 — Simulation\n// ---------------------------------------------------------------------------\n\n/**\n * Observation and question list of one simulado, loaded when its card is\n * first expanded.\n */\nexport function SimulationAnswers({\n detail,\n note,\n onRetryNote,\n notePlaceholder,\n onSaveNote,\n onSaveQuestionComment,\n}: {\n readonly detail: SimulationDetailState | undefined;\n readonly note: SimulationNoteState | undefined;\n /** Load the observation again after a failed request */\n readonly onRetryNote: () => void;\n /** Placeholder of the observation editor; see `SimulationNoteRowProps` */\n readonly notePlaceholder?: string;\n readonly onSaveNote: SimulationNoteRowProps['onSave'];\n readonly onSaveQuestionComment: (\n questionId: string,\n comment: string\n ) => Promise<void>;\n}) {\n if (!detail || detail.loading) {\n // O `SkeletonCard` já é `role=\"status\"` e anuncia o carregamento.\n return <SkeletonCard className=\"h-40\" />;\n }\n\n if (detail.error) {\n return (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {detail.error}\n </Text>\n );\n }\n\n if (!detail.data) return null;\n\n return (\n <>\n <SimulationNoteRow\n note={note?.data ?? null}\n loading={note?.loading ?? false}\n loadError={note?.error ?? null}\n onRetry={onRetryNote}\n placeholder={notePlaceholder}\n onSave={onSaveNote}\n />\n\n <div className=\"flex flex-col gap-2 pt-2\">\n <Text as=\"h4\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Respostas\n </Text>\n {detail.data.questions.map((question, qIndex) => (\n <SimulationQuestionItem\n key={question.questionId}\n question={question}\n index={qIndex}\n onSaveComment={(comment) =>\n onSaveQuestionComment(question.questionId, comment)\n }\n />\n ))}\n </div>\n </>\n );\n}\n\nfunction SimulationItem({\n simulation,\n index,\n expanded,\n onToggle,\n detail,\n note,\n onRetryNote,\n onSaveNote,\n onSaveQuestionComment,\n}: SimulationCardHandlers & {\n readonly simulation: StudentSimulationItem;\n readonly index: number;\n readonly expanded: boolean;\n readonly onToggle: () => void;\n}) {\n const title = simulation.title?.trim()\n ? simulation.title.trim()\n : `Simulado ${index + 1}`;\n const meta = buildSimulationMeta(simulation);\n\n return (\n <SimulationCardShell\n value={simulation.id}\n title={title}\n meta={meta}\n correct={simulation.correctCount}\n totalQuestions={simulation.totalQuestions}\n expanded={expanded}\n onToggle={onToggle}\n >\n <SimulationStatCards\n score={simulation.score}\n correct={simulation.correctCount}\n incorrect={simulation.incorrectCount}\n blank={simulation.blankCount}\n />\n {/* Essays awaiting grading used to be counted as blank. */}\n {detail?.data && detail.data.counts.pending > 0 && (\n <Text size=\"sm\" className=\"text-text-600\">\n {`${detail.data.counts.pending} ${detail.data.counts.pending === 1 ? 'questão dissertativa aguarda' : 'questões dissertativas aguardam'} correção`}\n </Text>\n )}\n <ContentCards\n best={simulation.bestContent ?? null}\n worst={simulation.worstContent ?? null}\n />\n {expanded && (\n <SimulationAnswers\n detail={detail}\n note={note}\n onRetryNote={onRetryNote}\n onSaveNote={onSaveNote}\n onSaveQuestionComment={onSaveQuestionComment}\n />\n )}\n </SimulationCardShell>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Modal\n// ---------------------------------------------------------------------------\n\n/**\n * Modal that shows a single student's answered simulations as a nested\n * accordion (simulation -> question -> options), lazily loading each\n * simulation's detail when it is expanded. Reuses the shared StatCard and\n * AlternativesList components so the detail matches the activity correction UI.\n */\nexport function SimulationsDetailModal({\n api,\n isOpen,\n onClose,\n student,\n}: SimulationsDetailModalProps) {\n const useSimulations = useMemo(() => createUseSimulations(api), [api]);\n const { fetchStudentSimulations } = useSimulations();\n\n const [list, setList] = useState<SimulationsListData | null>(null);\n const [listLoading, setListLoading] = useState(false);\n const [listError, setListError] = useState<string | null>(null);\n\n const {\n expandedId,\n details,\n notes,\n toggle,\n retryNote,\n makeSaveNote,\n makeSaveQuestionComment,\n } = useSimulationCardDetails({\n api,\n userInstitutionId: student?.userInstitutionId ?? null,\n isOpen,\n });\n\n useEffect(() => {\n if (!isOpen || !student) return;\n\n setList(null);\n setListLoading(true);\n setListError(null);\n\n let active = true;\n fetchStudentSimulations(student.userInstitutionId)\n .then((data) => {\n if (active) setList(data);\n })\n .catch(() => {\n if (active) setListError('Erro ao carregar os simulados do estudante');\n })\n .finally(() => {\n if (active) setListLoading(false);\n });\n\n return () => {\n active = false;\n };\n }, [isOpen, student, fetchStudentSimulations]);\n\n return (\n <Modal isOpen={isOpen} onClose={onClose} title=\"Simulados\" size=\"xl\">\n {student && (\n // Região nomeada. Sem `tabIndex`: os navegadores atuais já tornam\n // focável pelo teclado um contêiner rolável sem controle focável.\n <section\n className=\"flex max-h-[70vh] flex-col gap-6 overflow-y-auto pr-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-950\"\n aria-label={`Simulados de ${student.name}`}\n >\n <StudentSummaryHeader\n name={student.name}\n location={[\n list?.student.school,\n list?.student.class,\n list?.student.schoolYear,\n ]}\n />\n\n {listLoading && (\n <>\n <SkeletonCard className=\"h-20\" />\n <SkeletonCard className=\"h-40\" />\n </>\n )}\n {listError && (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {listError}\n </Text>\n )}\n\n {list && (\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>Dados de simulados</SectionTitle>\n <div className=\"flex flex-col gap-2 md:flex-row\">\n <DataCard\n label=\"Simulados realizados\"\n value={String(list.student.simulationsAnswered)}\n />\n {list.student.totalTimeSeconds !== undefined && (\n <DataCard\n label=\"Tempo total\"\n value={formatTimeSpent(list.student.totalTimeSeconds)}\n />\n )}\n </div>\n </section>\n )}\n\n {list && (\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>Simulados realizados</SectionTitle>\n {list.simulations.data.length === 0 ? (\n <div className=\"flex items-center justify-center rounded-xl border border-border-50 bg-background p-6\">\n <Text size=\"sm\" className=\"text-text-600\">\n Este estudante ainda não respondeu nenhum simulado.\n </Text>\n </div>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {list.simulations.data.map((simulation, index) => (\n <SimulationItem\n // Keyed by the student too: the comment fields below keep\n // a dirty draft through a `value` change so an in-flight\n // save cannot discard it, and a note written for one\n // student must never survive into another. Remounting\n // resets it for free.\n key={`${student.userInstitutionId}-${simulation.id}`}\n simulation={simulation}\n index={index}\n expanded={expandedId === simulation.id}\n onToggle={() => toggle(simulation.id)}\n detail={details[simulation.id]}\n note={notes[simulation.id]}\n onRetryNote={() => retryNote(simulation.id)}\n onSaveNote={makeSaveNote(simulation.id)}\n onSaveQuestionComment={makeSaveQuestionComment(\n simulation.id\n )}\n />\n ))}\n </div>\n )}\n </section>\n )}\n </section>\n )}\n </Modal>\n );\n}\n","import { formatTimeSpent } from './activityDetailsUtils';\n\n/**\n * Format the time a student spent on a single question, the way the relatórios\n * of simulados present it: `HH:MM:SS` (`00:00:40`, `00:02:30`, `01:05:30`).\n *\n * The clock format is shared with `formatTimeSpent`, which renders the activity\n * and simulado totals — a question row and the \"Tempo total\" card above it read\n * the same way instead of each inventing its own notation.\n *\n * What this adds on top is the \"not measured\" rule: it returns `null` when\n * there is nothing meaningful to show — the value is zero, absent, negative or\n * not finite. Zero is not the same as \"answered instantly\": simulations\n * answered before per-question telemetry existed carry a zero, and so does a\n * question the student never opened. Rendering `00:00:00` there would read as\n * \"the student guessed\", which is the opposite of what the teacher should\n * conclude. Callers drop the whole segment from the row instead.\n *\n * @param seconds - Time spent in seconds\n * @returns Formatted duration in `HH:MM:SS`, or null when there is no measured time\n *\n * @example\n * ```typescript\n * formatQuestionDuration(150); // \"00:02:30\"\n * formatQuestionDuration(0); // null\n * ```\n */\nexport function formatQuestionDuration(\n seconds: number | null | undefined\n): string | null {\n if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {\n return null;\n }\n\n // Truncate before the zero check, not after: a fractional value below one\n // second floors to 0 and must take the same \"not measured\" path as a plain\n // 0, otherwise it would render a misleading `00:00:00`.\n const total = Math.floor(seconds);\n if (total <= 0) {\n return null;\n }\n\n return formatTimeSpent(total);\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { BaseApiClient } from '../types/api';\nimport type { NoteData, SimulationDetailData } from '../types/simulations';\nimport { createUseSimulations } from './useSimulations';\n\n/** Lazily loaded question detail of one simulado. */\nexport interface SimulationDetailState {\n readonly loading: boolean;\n readonly error: string | null;\n readonly data: SimulationDetailData | null;\n}\n\n/**\n * Lazily loaded teacher observation of one simulado.\n *\n * `error` exists so a failed request is never shown as \"this simulado has no\n * observation\": that would offer the teacher an empty field whose save is a\n * create-or-replace, silently overwriting a note they were never shown.\n */\nexport interface SimulationNoteState {\n readonly loading: boolean;\n readonly error: string | null;\n readonly data: NoteData | null;\n}\n\n/** Save handler of one simulado's observation, as the note row expects it. */\nexport type SaveSimulationNote = (\n text: string,\n file: File | null,\n existingAttachment: string | null\n) => Promise<void>;\n\n/**\n * What one simulado card needs from this hook: the two loaded pieces and the\n * three actions on them, already bound to that simulado.\n */\nexport interface SimulationCardHandlers {\n readonly detail: SimulationDetailState | undefined;\n readonly note: SimulationNoteState | undefined;\n /** Load the observation again after a failed request */\n readonly onRetryNote: () => void;\n readonly onSaveNote: SaveSimulationNote;\n readonly onSaveQuestionComment: (\n questionId: string,\n comment: string\n ) => Promise<void>;\n}\n\nexport interface UseSimulationCardDetailsParams {\n /** API client used to load and save the per-simulado data */\n readonly api: BaseApiClient;\n /** Enrollment whose simulados are listed; null while there is no student */\n readonly userInstitutionId: string | null;\n /** Whether the modal is open; reopening starts a fresh session */\n readonly isOpen: boolean;\n /**\n * Whether expanding a card loads its questions and observation.\n *\n * Those two come from `/performance/simulations/students/...`, which the\n * backend answers for simulados and activities but not for every activity\n * type. A modal listing a type it does not serve still expands its cards —\n * the stat and content cards travel with the list — but must not ask for a\n * detail that would 404. Default true.\n */\n readonly enabled?: boolean;\n}\n\nexport interface UseSimulationCardDetailsReturn {\n readonly expandedId: string | null;\n readonly details: Record<string, SimulationDetailState>;\n readonly notes: Record<string, SimulationNoteState>;\n /** Expand or collapse one simulado, loading its data the first time */\n readonly toggle: (simulationId: string) => void;\n /** Load the observation again after a failed request */\n readonly retryNote: (simulationId: string) => void;\n readonly makeSaveNote: (simulationId: string) => SaveSimulationNote;\n readonly makeSaveQuestionComment: (\n simulationId: string\n ) => (questionId: string, comment: string) => Promise<void>;\n}\n\n/**\n * Per-simulado detail state of a student's simulados modal.\n *\n * Owns what both modals that list simulados need: which card is expanded, the\n * questions and observation of each expanded card, and the two save actions.\n * The list itself stays with the caller, because each modal gets it from a\n * different endpoint.\n *\n * Responses are tagged with a session. The session changes whenever the modal\n * opens, the student changes or the API client changes, and a response from an\n * older session is dropped instead of written into the current one.\n *\n * @param params - API client, the enrollment being shown and the open flag\n * @returns Expanded card, loaded details and notes, and the actions on them\n *\n * @example\n * ```tsx\n * const { expandedId, details, notes, toggle } = useSimulationCardDetails({\n * api,\n * userInstitutionId: student?.userInstitutionId ?? null,\n * isOpen,\n * });\n * ```\n */\nexport function useSimulationCardDetails({\n api,\n userInstitutionId,\n isOpen,\n enabled = true,\n}: UseSimulationCardDetailsParams): UseSimulationCardDetailsReturn {\n const useSimulations = useMemo(() => createUseSimulations(api), [api]);\n const {\n fetchSimulationDetail,\n fetchNote,\n uploadNoteAttachment,\n saveNote,\n saveQuestionComment,\n } = useSimulations();\n\n const [expandedId, setExpandedId] = useState<string | null>(null);\n const [details, setDetails] = useState<Record<string, SimulationDetailState>>(\n {}\n );\n const [notes, setNotes] = useState<Record<string, SimulationNoteState>>({});\n\n // Guards state updates against the modal unmounting mid-request.\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // Bumped on every new session, so a slow response from the previous student\n // (or from a replaced API client) is dropped instead of written into the\n // current one.\n const sessionRef = useRef(0);\n useEffect(() => {\n sessionRef.current += 1;\n setExpandedId(null);\n setDetails({});\n setNotes({});\n }, [isOpen, userInstitutionId, api]);\n\n const isStale = useCallback(\n (session: number) => !mountedRef.current || sessionRef.current !== session,\n []\n );\n\n const loadDetail = useCallback(\n (enrollmentId: string, simulationId: string) => {\n const session = sessionRef.current;\n setDetails((previous) => ({\n ...previous,\n [simulationId]: { loading: true, error: null, data: null },\n }));\n fetchSimulationDetail(enrollmentId, simulationId)\n .then((data) => {\n if (isStale(session)) return;\n setDetails((previous) => ({\n ...previous,\n [simulationId]: { loading: false, error: null, data },\n }));\n })\n .catch(() => {\n if (isStale(session)) return;\n setDetails((previous) => ({\n ...previous,\n [simulationId]: {\n loading: false,\n error: 'Erro ao carregar as respostas',\n data: null,\n },\n }));\n });\n },\n [fetchSimulationDetail, isStale]\n );\n\n const loadNote = useCallback(\n (enrollmentId: string, simulationId: string) => {\n const session = sessionRef.current;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: { loading: true, error: null, data: null },\n }));\n fetchNote(enrollmentId, simulationId)\n .then((data) => {\n if (isStale(session)) return;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: { loading: false, error: null, data },\n }));\n })\n .catch(() => {\n if (isStale(session)) return;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: {\n loading: false,\n error: 'Erro ao carregar a observação',\n data: null,\n },\n }));\n });\n },\n [fetchNote, isStale]\n );\n\n const toggle = useCallback(\n (simulationId: string) => {\n if (!userInstitutionId) return;\n const next = expandedId === simulationId ? null : simulationId;\n setExpandedId(next);\n // Collapsing loads nothing; so does a list whose items have no detail\n // endpoint behind them (see `enabled`).\n if (!next || !enabled) return;\n\n // The two requests are guarded separately: a loaded detail must not stop\n // the observation from being fetched, and the other way around.\n if (!details[simulationId]) loadDetail(userInstitutionId, simulationId);\n if (!notes[simulationId]) loadNote(userInstitutionId, simulationId);\n },\n [\n userInstitutionId,\n enabled,\n expandedId,\n details,\n notes,\n loadDetail,\n loadNote,\n ]\n );\n\n // `enabled` can turn on under an already expanded card — a caller that\n // decides it from the list's kind may flip it without touching the card.\n // `toggle` ran while disabled and loaded nothing, so this catches the open\n // card up; the same guards keep it from repeating a request `toggle` made.\n useEffect(() => {\n if (!enabled || !expandedId || !userInstitutionId) return;\n if (!details[expandedId]) loadDetail(userInstitutionId, expandedId);\n if (!notes[expandedId]) loadNote(userInstitutionId, expandedId);\n }, [\n enabled,\n expandedId,\n userInstitutionId,\n details,\n notes,\n loadDetail,\n loadNote,\n ]);\n\n const retryNote = useCallback(\n (simulationId: string) => {\n if (!userInstitutionId) return;\n loadNote(userInstitutionId, simulationId);\n },\n [userInstitutionId, loadNote]\n );\n\n /**\n * Save the observation of one simulado, uploading a newly chosen file first\n * so only its public URL travels with the note.\n */\n const makeSaveNote = useCallback(\n (simulationId: string): SaveSimulationNote =>\n async (text, file, existingAttachment) => {\n if (!userInstitutionId) return;\n const session = sessionRef.current;\n const attachment = file\n ? await uploadNoteAttachment(file)\n : existingAttachment;\n const saved = await saveNote(\n userInstitutionId,\n simulationId,\n text,\n attachment\n );\n if (isStale(session)) return;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: { loading: false, error: null, data: saved },\n }));\n },\n [userInstitutionId, uploadNoteAttachment, saveNote, isStale]\n );\n\n /**\n * Save a comment on one question and reflect it in the loaded detail, so the\n * field shows what the server now holds without a refetch.\n */\n const makeSaveQuestionComment = useCallback(\n (simulationId: string) => async (questionId: string, comment: string) => {\n if (!userInstitutionId) return;\n const session = sessionRef.current;\n const saved = await saveQuestionComment(\n userInstitutionId,\n simulationId,\n questionId,\n comment\n );\n if (isStale(session)) return;\n setDetails((previous) => {\n const current = previous[simulationId];\n if (!current?.data) return previous;\n return {\n ...previous,\n [simulationId]: {\n ...current,\n data: {\n ...current.data,\n questions: current.data.questions.map((question) =>\n question.questionId === questionId\n ? { ...question, teacherComment: saved?.teacherComment ?? '' }\n : question\n ),\n },\n },\n };\n });\n },\n [userInstitutionId, saveQuestionComment, isStale]\n );\n\n return {\n expandedId,\n details,\n notes,\n toggle,\n retryNote,\n makeSaveNote,\n makeSaveQuestionComment,\n };\n}\n","/**\n * Types of the student simulations details modal.\n *\n * They mirror `POST /performance/simulated/activities/student-simulations`,\n * the teacher/manager-scoped endpoint that answers everything the modal shows\n * for one student under a given simulado cut and time window.\n */\n\n/**\n * Performance band of a student, as classified by the server.\n *\n * With `performanceMode=absolute` the server derives the band from the\n * percentage: >=90 HIGHLIGHT, >=70 ABOVE_AVERAGE, >=40 BELOW_AVERAGE, else\n * ATTENTION_POINT. See {@link SIMULATED_SIMULATIONS_TAG_CONFIG}.\n */\nexport type SimulatedPerformanceTag =\n | 'HIGHLIGHT'\n | 'ABOVE_AVERAGE'\n | 'BELOW_AVERAGE'\n | 'ATTENTION_POINT';\n\n/**\n * Label and badge colours per band.\n *\n * The colours are the gestor's map-token palette for the same bands, so the\n * ranking tables, the distribution pie and this badge all read alike.\n *\n * Deliberately separate from `SIMULATED_PERFORMANCE_TAG_CONFIG` in\n * `SimulatedStudentDetailsModal`: that one maps a band to a `variant` for the\n * ENEM stack's own badge, while this one carries explicit classes. Merging them\n * would repaint the ENEM screens.\n */\nexport const SIMULATED_SIMULATIONS_TAG_CONFIG: Record<\n SimulatedPerformanceTag,\n { label: string; badgeClassName: string }\n> = {\n HIGHLIGHT: {\n label: 'Destaque da turma',\n badgeClassName: 'bg-success-300 text-text',\n },\n ABOVE_AVERAGE: {\n label: 'Acima da média',\n badgeClassName: 'bg-indicator-positive text-warning-900',\n },\n BELOW_AVERAGE: {\n label: 'Abaixo da média',\n badgeClassName: 'bg-warning-400 text-text',\n },\n ATTENTION_POINT: {\n label: 'Ponto de atenção',\n badgeClassName: 'bg-error-700 text-text-100',\n },\n};\n\n/** A content (subtema) with the student's hit rate on it. */\nexport interface StudentContentHitRate {\n contentId: string;\n contentName: string;\n correct: number;\n totalQuestions: number;\n /** 0-100 */\n correctPercentage: number;\n}\n\n/**\n * One simulado the student answered, as the details modal lists it.\n *\n * `score` is a 0-100 percentage shown as a 0-10 grade; `answeredAt` is an ISO\n * string, null when the server has no date for the attempt.\n */\nexport interface StudentSimulationItem {\n activityId: string;\n title: string;\n subtype: string | null;\n score: number;\n timeSpentSeconds: number;\n answeredAt: string | null;\n correct: number;\n incorrect: number;\n blank: number;\n totalQuestions: number;\n bestContent: StudentContentHitRate | null;\n worstContent: StudentContentHitRate | null;\n}\n\n/** Everything the student details modal shows, for one student and one cut. */\nexport interface StudentSimulationsData {\n student: {\n studentId: string;\n userInstitutionId: string;\n institutionId: string | null;\n name: string;\n school: string | null;\n schoolYear: string | null;\n class: string | null;\n /** 0-100, averaged over the listed simulados */\n average: number;\n performance: SimulatedPerformanceTag;\n };\n totals: {\n simulationsCount: number;\n totalTimeSeconds: number;\n correct: number;\n incorrect: number;\n blank: number;\n };\n bestContent: StudentContentHitRate | null;\n worstContent: StudentContentHitRate | null;\n simulations: StudentSimulationItem[];\n /**\n * Activities assigned in the period the student has not answered yet.\n * Listed after the answered ones; absent (or empty) lists nothing extra.\n */\n pending?: StudentPendingActivity[];\n /**\n * The student's logins in the period. Rendered as its own section when\n * present; the Simulados report leaves it out.\n */\n access?: StudentAccessSummary;\n}\n\n/** One activity assigned to the student and still unanswered. */\nexport interface StudentPendingActivity {\n activityId: string;\n title: string;\n subtype: string | null;\n}\n\n/** How present the student was in the period. */\nexport interface StudentAccessSummary {\n accessCount: number;\n /** Minutes across activities and lessons, as the access report counts. */\n totalTimeMinutes: number;\n /** ISO string of the last login, null when the student never logged in. */\n lastAccess: string | null;\n}\n","import type { ReactNode } from 'react';\nimport Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport { SkeletonCard } from '../Skeleton/Skeleton';\nimport { SimulationAnswers } from '../SimulationsPage/SimulationsDetailModal';\nimport {\n buildSimulationMeta,\n ContentCards,\n DataCard,\n SectionTitle,\n SimulationCardShell,\n SimulationStatCards,\n StudentSummaryHeader,\n} from '../shared/SimulationSummaryCards';\nimport {\n useSimulationCardDetails,\n type SimulationCardHandlers,\n} from '../../hooks/useSimulationCardDetails';\nimport type { BaseApiClient } from '../../types/api';\nimport { formatTimeSpent } from '../../utils/activityDetailsUtils';\nimport dayjs from 'dayjs';\nimport {\n SIMULATED_SIMULATIONS_TAG_CONFIG,\n type SimulatedPerformanceTag,\n type StudentAccessSummary,\n type StudentPendingActivity,\n type StudentSimulationItem,\n type StudentSimulationsData,\n} from './types';\n\n/**\n * Band badge, in the gestor tables' palette.\n *\n * Exported so a consuming table can label its Desempenho column with exactly\n * the badge the modal header shows.\n */\nexport function PerformanceBadge({\n tag,\n}: {\n readonly tag: SimulatedPerformanceTag;\n}) {\n const config = SIMULATED_SIMULATIONS_TAG_CONFIG[tag];\n if (!config) return null;\n\n return (\n <Badge\n variant=\"solid\"\n action=\"neutral\"\n size=\"small\"\n className={config.badgeClassName}\n >\n {config.label}\n </Badge>\n );\n}\n\n/**\n * The copy that names what the list holds.\n *\n * The modal was written for simulados and its defaults say so; the teacher's\n * Atividades report lists activities through the same endpoint and hands over\n * its own wording. Only these four strings differ between the two — the\n * \"Tempo total\" and \"Desempenho geral\" blocks read the same either way.\n */\nexport interface SimulatedStudentSimulationsModalLabels {\n /** Title of the first section, e.g. \"Dados de simulados\" */\n readonly dataSection: string;\n /** Label of the count card, e.g. \"Simulados realizados\" */\n readonly completedCount: string;\n /**\n * Label of a third card of the data section, with the questions the\n * student answered (correct + incorrect + blank). Left out, the card is\n * not rendered — the Simulados report does not show it.\n */\n readonly questionsAnswered?: string;\n /** Title of the list section, e.g. \"Simulados realizados\" */\n readonly listTitle: string;\n /** Shown when the list is empty, e.g. \"Nenhum simulado no período\" */\n readonly empty: string;\n /** Title of the logins section, rendered when `data.access` is present */\n readonly accessSection: string;\n readonly accessCount: string;\n readonly timeOnline: string;\n readonly lastLogin: string;\n /** Shown inside the card of an assigned activity not answered yet */\n readonly pendingMessage: string;\n /** Placeholder of the observation editor of an expanded card */\n readonly notePlaceholder: string;\n}\n\n/** The simulado wording, applied when the caller names nothing. */\nexport const DEFAULT_SIMULATED_STUDENT_SIMULATIONS_LABELS: SimulatedStudentSimulationsModalLabels =\n {\n dataSection: 'Dados de simulados',\n completedCount: 'Simulados realizados',\n listTitle: 'Simulados realizados',\n empty: 'Nenhum simulado no período',\n accessSection: 'Dados de acesso',\n accessCount: 'Quantidade de acessos',\n timeOnline: 'Tempo total online',\n lastLogin: 'Último login',\n pendingMessage: 'Sem dados ainda! A atividade ainda não foi feita.',\n notePlaceholder: 'Escreva uma observação para este simulado',\n };\n\n/** A count as the design writes it: at least two digits, so 6 reads \"06\". */\nfunction formatCount(value: number): string {\n return String(value).padStart(2, '0');\n}\n\n/**\n * \"dd/mm/aaaa • HH:mmh\" of the last login. A student who never logged in\n * gets the zeroed date the design uses as its placeholder, not a dash.\n */\nfunction formatLastAccess(lastAccess: string | null): string {\n return lastAccess\n ? dayjs(lastAccess).format('DD/MM/YYYY • HH:mm[h]')\n : '00/00/0000 • 00:00h';\n}\n\n/** What the list shows when there is nothing to list, answered or pending. */\nexport interface SimulatedStudentSimulationsEmptyState {\n /** Illustration above the title; omitted, only the text is shown */\n readonly image?: string;\n readonly title: string;\n readonly description: string;\n}\n\n/**\n * Empty list with an illustration: the teacher has not assigned anything to\n * this student yet, so there is nothing to answer, let alone to show.\n */\nfunction ListEmptyState({\n image,\n title,\n description,\n}: SimulatedStudentSimulationsEmptyState) {\n return (\n <div className=\"flex min-h-[250px] flex-col items-center justify-center gap-4 rounded-xl border border-border-50 bg-background p-6\">\n {image && <img src={image} alt=\"\" className=\"h-20 w-auto\" />}\n <div className=\"flex flex-col items-center gap-1\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-center text-text-950\">\n {title}\n </Text>\n <Text size=\"xs\" className=\"text-center text-text-500\">\n {description}\n </Text>\n </div>\n </div>\n );\n}\n\n/** The logins section: how often, for how long and when last. */\nfunction AccessSection({\n access,\n labels,\n}: {\n readonly access: StudentAccessSummary;\n readonly labels: SimulatedStudentSimulationsModalLabels;\n}) {\n return (\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>{labels.accessSection}</SectionTitle>\n <div className=\"flex flex-col gap-2 md:flex-row\">\n <DataCard\n label={labels.accessCount}\n value={formatCount(access.accessCount)}\n />\n <DataCard\n label={labels.timeOnline}\n value={formatTimeSpent(access.totalTimeMinutes * 60)}\n />\n <DataCard\n label={labels.lastLogin}\n value={formatLastAccess(access.lastAccess)}\n />\n </div>\n </section>\n );\n}\n\n/**\n * One assigned activity the student has not answered yet.\n *\n * Same frame as the answered cards, but nothing to expand: there is no score,\n * no answers and no date, only the title and the reason the card is empty.\n */\nfunction PendingCard({\n activity,\n message,\n}: {\n readonly activity: StudentPendingActivity;\n readonly message: string;\n}) {\n return (\n <div className=\"flex flex-col gap-2 rounded-xl border border-border-200 bg-background p-4\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n {activity.title}\n </Text>\n <Text size=\"xs\" className=\"text-text-500\">\n {message}\n </Text>\n </div>\n );\n}\n\n/** One expandable simulado of the \"Simulados realizados\" list. */\nfunction SimulationCard({\n simulation,\n expanded,\n showAnswers,\n notePlaceholder,\n onToggle,\n detail,\n note,\n onRetryNote,\n onSaveNote,\n onSaveQuestionComment,\n}: SimulationCardHandlers & {\n readonly simulation: StudentSimulationItem;\n readonly expanded: boolean;\n /** Whether the expanded card lists the questions and the observation */\n readonly showAnswers: boolean;\n /** Placeholder of the observation editor */\n readonly notePlaceholder: string;\n readonly onToggle: () => void;\n}) {\n return (\n <SimulationCardShell\n value={simulation.activityId}\n title={simulation.title}\n meta={buildSimulationMeta(simulation)}\n correct={simulation.correct}\n totalQuestions={simulation.totalQuestions}\n expanded={expanded}\n onToggle={onToggle}\n >\n <SimulationStatCards\n score={simulation.score}\n correct={simulation.correct}\n incorrect={simulation.incorrect}\n blank={simulation.blank}\n />\n <ContentCards\n best={simulation.bestContent}\n worst={simulation.worstContent}\n />\n {expanded && showAnswers && (\n <SimulationAnswers\n detail={detail}\n note={note}\n onRetryNote={onRetryNote}\n notePlaceholder={notePlaceholder}\n onSaveNote={onSaveNote}\n onSaveQuestionComment={onSaveQuestionComment}\n />\n )}\n </SimulationCardShell>\n );\n}\n\n/** Loading placeholder of the whole modal. */\nfunction ModalSkeleton() {\n return (\n <div className=\"flex flex-col gap-4\">\n <SkeletonCard className=\"min-h-[72px]\" />\n <SkeletonCard className=\"min-h-[120px]\" />\n <SkeletonCard className=\"min-h-[200px]\" />\n </div>\n );\n}\n\n/**\n * Student details modal of the Simulados report, opened by clicking a row of\n * \"Desempenho por estudante\".\n *\n * The summary data, loading and error are owned by the caller: that request\n * depends on the tab and the time window the report is on, and the report\n * already knows both. The `title` arrives ready for the same reason — the\n * professor app names a fixed period while the gestor may name an arbitrary\n * calendar range. What this modal owns is the per-simulado detail (observation\n * and questions), fetched from the Simulados endpoints when a card is first\n * expanded.\n */\nexport interface SimulatedStudentSimulationsModalProps {\n /** API client used to load each simulado's questions and observation */\n readonly api: BaseApiClient;\n readonly isOpen: boolean;\n readonly onClose: () => void;\n /** Modal title, e.g. \"Simulados Enem em 1 mês\" */\n readonly title: string;\n readonly data: StudentSimulationsData | null;\n readonly loading: boolean;\n readonly error: string | null;\n /** Wording of the list; whatever is omitted keeps the simulado copy */\n readonly labels?: Partial<SimulatedStudentSimulationsModalLabels>;\n /**\n * Whether an expanded card loads and lists its questions and observation.\n * Off for lists of a type the detail endpoint does not serve (it answers\n * for simulados and activities), so the card shows its stat and content\n * cards alone. Default true.\n */\n readonly detailsEnabled?: boolean;\n /** Where the band badge sits in the header. Default `start`. */\n readonly badgePlacement?: 'start' | 'end';\n /**\n * Whether to render the logins section from `data.access`. Off by default:\n * the endpoint answers it for every caller, but only the Atividades\n * ranking modal shows it. Default false.\n */\n readonly showAccess?: boolean;\n /**\n * Whether to list the assignments of `data.pending` after the answered\n * ones. Off by default, for the same reason. Default false.\n */\n readonly showPending?: boolean;\n /**\n * Illustrated empty state of the list, shown when the student has nothing\n * answered and nothing pending. Omitted, the list shows `labels.empty`\n * as a single line.\n */\n readonly emptyState?: SimulatedStudentSimulationsEmptyState;\n}\n\nexport function SimulatedStudentSimulationsModal({\n api,\n isOpen,\n onClose,\n title,\n data,\n loading,\n error,\n labels,\n detailsEnabled = true,\n badgePlacement = 'start',\n showAccess = false,\n showPending = false,\n emptyState,\n}: SimulatedStudentSimulationsModalProps) {\n const copy = { ...DEFAULT_SIMULATED_STUDENT_SIMULATIONS_LABELS, ...labels };\n const {\n expandedId,\n details,\n notes,\n toggle,\n retryNote,\n makeSaveNote,\n makeSaveQuestionComment,\n } = useSimulationCardDetails({\n api,\n userInstitutionId: data?.student.userInstitutionId ?? null,\n isOpen,\n enabled: detailsEnabled,\n });\n\n let content: ReactNode = null;\n if (loading) {\n content = <ModalSkeleton />;\n } else if (error) {\n content = (\n // `role=\"alert\"`: a falha substitui o conteúdo e precisa ser anunciada.\n <div\n className=\"flex min-h-[160px] items-center justify-center\"\n role=\"alert\"\n >\n <Text size=\"sm\" className=\"text-text-500\">\n {error}\n </Text>\n </div>\n );\n } else if (data) {\n const pending = showPending ? (data.pending ?? []) : [];\n // Nothing answered in the period: the totals are all zero and the\n // average is meaningless, so the cards show placeholders, not results.\n const hasAnswers = data.simulations.length > 0;\n const listIsEmpty = !hasAnswers && pending.length === 0;\n content = (\n <div className=\"flex flex-col gap-6\">\n <StudentSummaryHeader\n name={data.student.name}\n location={[\n data.student.school,\n data.student.class,\n data.student.schoolYear,\n ]}\n badge={<PerformanceBadge tag={data.student.performance} />}\n badgePlacement={badgePlacement}\n />\n\n {showAccess && data.access && (\n <AccessSection access={data.access} labels={copy} />\n )}\n\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>{copy.dataSection}</SectionTitle>\n <div className=\"flex flex-col gap-2 md:flex-row\">\n <DataCard\n label={copy.completedCount}\n value={\n hasAnswers\n ? String(data.totals.simulationsCount)\n : formatCount(0)\n }\n />\n {copy.questionsAnswered && (\n <DataCard\n label={copy.questionsAnswered}\n value={String(\n data.totals.correct +\n data.totals.incorrect +\n data.totals.blank\n )}\n />\n )}\n <DataCard\n label=\"Tempo total\"\n value={formatTimeSpent(data.totals.totalTimeSeconds)}\n />\n </div>\n </section>\n\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>Desempenho geral</SectionTitle>\n <SimulationStatCards\n score={data.student.average}\n correct={data.totals.correct}\n incorrect={data.totals.incorrect}\n blank={data.totals.blank}\n empty={!hasAnswers}\n />\n <ContentCards best={data.bestContent} worst={data.worstContent} />\n </section>\n\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>{copy.listTitle}</SectionTitle>\n {listIsEmpty && emptyState && <ListEmptyState {...emptyState} />}\n {listIsEmpty && !emptyState && (\n <div className=\"flex items-center justify-center rounded-xl border border-border-50 bg-background p-6\">\n <Text size=\"sm\" className=\"text-text-500\">\n {copy.empty}\n </Text>\n </div>\n )}\n {!listIsEmpty && (\n <div className=\"flex flex-col gap-2\">\n {data.simulations.map((simulation) => (\n <SimulationCard\n // Keyed by the student too: the comment fields keep a dirty\n // draft through a `value` change, and a draft written for\n // one student must never survive into another.\n key={`${data.student.userInstitutionId}-${simulation.activityId}`}\n simulation={simulation}\n expanded={expandedId === simulation.activityId}\n showAnswers={detailsEnabled}\n notePlaceholder={copy.notePlaceholder}\n onToggle={() => toggle(simulation.activityId)}\n detail={details[simulation.activityId]}\n note={notes[simulation.activityId]}\n onRetryNote={() => retryNote(simulation.activityId)}\n onSaveNote={makeSaveNote(simulation.activityId)}\n onSaveQuestionComment={makeSaveQuestionComment(\n simulation.activityId\n )}\n />\n ))}\n {pending.map((activity) => (\n <PendingCard\n key={`${data.student.userInstitutionId}-pending-${activity.activityId}`}\n activity={activity}\n message={copy.pendingMessage}\n />\n ))}\n </div>\n )}\n </section>\n </div>\n );\n }\n\n return (\n <Modal isOpen={isOpen} onClose={onClose} title={title} size=\"xl\">\n {content}\n </Modal>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-AZKNYREH.js","../src/utils/questionRenderer/components/index.tsx","../src/components/shared/TrueFalseStatementList.tsx","../src/utils/studentActivityCorrection/constants.ts","../src/utils/studentActivityCorrection/utils.ts","../src/utils/studentActivityCorrection/subtopics.ts","../src/utils/studentActivityCorrection/performanceTag.ts","../src/utils/studentActivityCorrection/converter.ts","../src/components/SimulationsPage/SimulationsDetailModal.tsx","../src/utils/questionDuration.ts","../src/hooks/useSimulationCardDetails.ts","../src/components/SimulatedStudentSimulationsModal/types.ts","../src/components/SimulatedStudentSimulationsModal/SimulatedStudentSimulationsModal.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-AZKNYREH.js","sourcesContent":[null,"import { Fragment, useId, type ReactNode } from 'react';\nimport Badge from '../../../components/Badge/Badge';\nimport { CheckCircleIcon } from '@phosphor-icons/react/dist/csr/CheckCircle';\nimport { XCircleIcon } from '@phosphor-icons/react/dist/csr/XCircle';\nimport { cn } from '../../utils';\nimport Text from '../../../components/Text/Text';\nimport type { QuestionRendererProps } from '../types';\nimport { stripHtmlTags } from '../../stringUtils';\nimport { OptionStatus } from '../../../enums/Options';\n\n/**\n * Get status badge component\n */\nexport const getStatusBadge = (status?: OptionStatus): ReactNode => {\n switch (status) {\n case OptionStatus.CORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"success\"\n iconLeft={<CheckCircleIcon aria-hidden=\"true\" />}\n >\n Resposta correta\n </Badge>\n );\n case OptionStatus.INCORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconLeft={<XCircleIcon aria-hidden=\"true\" />}\n >\n Resposta incorreta\n </Badge>\n );\n default:\n return null;\n }\n};\n\n/**\n * Container component for question content\n */\nexport const QuestionContainer = ({\n children,\n className,\n}: {\n children: ReactNode;\n className?: string;\n}) => {\n return (\n <div\n className={cn(\n 'bg-background rounded-t-xl px-4 pt-4 pb-[80px] h-auto flex flex-col gap-4 mb-auto',\n className\n )}\n >\n {children}\n </div>\n );\n};\n\n/**\n * Subtitle component for question sections\n */\nexport const QuestionSubTitle = ({ subTitle }: { subTitle: string }) => {\n return (\n <div className=\"px-4 pb-2 pt-6\">\n <Text size=\"md\" weight=\"bold\" color=\"text-text-950\">\n {subTitle}\n </Text>\n </div>\n );\n};\n\n/**\n * Internal component for fill in the blanks question\n * Uses useId hook to generate unique IDs\n *\n * Data structure:\n * - additionalContent: HTML text with {placeholderId} placeholders\n * - fillAnswers: Record<placeholderId, selectedOptionId> (what student chose)\n * - options: Array<{ id, option }> where id is placeholderId AND correct optionId\n * - Correctness: selectedOptionId === placeholderId means correct\n */\nexport const FillQuestionContent = ({\n question,\n result,\n}: QuestionRendererProps) => {\n const baseId = useId();\n\n // Get additionalContent (contains HTML with {placeholderId} placeholders)\n // Falls back to question.statement for older questions that may not have additionalContent\n const additionalContent =\n result?.additionalContent ||\n question.additionalContent ||\n question.statement ||\n '';\n\n // Strip HTML tags for clean text rendering\n const cleanText = stripHtmlTags(additionalContent);\n\n // Get options array (id is placeholderId, option is the text)\n const options =\n (\n result as {\n options?: Array<{ id: string; option: string; isCorrect: boolean }>;\n }\n )?.options ||\n question.options ||\n [];\n\n // Build a map of optionId -> option text for quick lookup\n const optionTextMap: Record<string, string> = {};\n options.forEach((opt) => {\n optionTextMap[opt.id] = opt.option;\n });\n\n // Get fillAnswers: Record<placeholderId, selectedOptionId>\n const fillAnswers: Record<string, string> = {};\n try {\n const resultWithFill = result as {\n fillAnswers?: Record<string, string> | null;\n };\n if (resultWithFill?.fillAnswers) {\n Object.assign(fillAnswers, resultWithFill.fillAnswers);\n } else if (result?.answer) {\n const parsed =\n typeof result.answer === 'string'\n ? JSON.parse(result.answer)\n : result.answer;\n if (typeof parsed === 'object') {\n Object.assign(fillAnswers, parsed);\n }\n }\n } catch {\n // Ignore parse errors\n }\n\n // Helper to get option text by ID - returns null if not found\n const getOptionTextById = (optionId: string): string | null => {\n if (!optionId) return null;\n return optionTextMap[optionId] ?? null;\n };\n\n // Check if answer is correct (selectedOptionId === placeholderId)\n const isAnswerCorrect = (placeholderId: string): boolean => {\n const selectedOptionId = fillAnswers[placeholderId];\n return selectedOptionId === placeholderId;\n };\n\n // Render student answer badge\n const renderStudentBadge = (placeholderId: string) => {\n const selectedOptionId = fillAnswers[placeholderId];\n const selectedOptionText = getOptionTextById(selectedOptionId);\n const isCorrect = isAnswerCorrect(placeholderId);\n\n if (!selectedOptionId) {\n return (\n <span className=\"inline-block align-middle mx-1 my-1\">\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconRight={<XCircleIcon aria-hidden=\"true\" />}\n size=\"large\"\n className=\"py-1 px-2\"\n >\n Não respondido\n </Badge>\n </span>\n );\n }\n\n // Handle case where option ID exists but text is not found\n const displayText = selectedOptionText ?? 'Opção não encontrada';\n\n return (\n <span className=\"inline-block align-middle mx-1 my-1\">\n <Badge\n variant=\"solid\"\n action={isCorrect ? 'success' : 'error'}\n iconRight={\n isCorrect ? (\n <CheckCircleIcon aria-hidden=\"true\" />\n ) : (\n <XCircleIcon aria-hidden=\"true\" />\n )\n }\n size=\"large\"\n className=\"py-1 px-2\"\n >\n {displayText}\n {/* O certo/errado só aparece pela cor e pelo ícone; o leitor de\n tela precisa do resultado em texto (WCAG 1.4.1). */}\n <Text as=\"span\" className=\"sr-only\">\n {isCorrect ? ' — correta' : ' — incorreta'}\n </Text>\n </Badge>\n </span>\n );\n };\n\n // Render correct answer\n const renderCorrectAnswer = (placeholderId: string) => {\n // The placeholderId IS the correct option ID\n const correctOptionText = getOptionTextById(placeholderId);\n const displayText = correctOptionText ?? '[Resposta não disponível]';\n return (\n <span className=\"inline mx-1 text-success-600 font-semibold border-b-2 border-success-600\">\n {/* O destaque verde não chega ao leitor de tela: o prefixo diz que\n este é o gabarito da lacuna. */}\n <Text as=\"span\" className=\"sr-only\">\n Resposta esperada:{' '}\n </Text>\n {displayText}\n </span>\n );\n };\n\n // Parse text and render with elements\n const renderTextWithElements = (text: string, isCorrectAnswer: boolean) => {\n const elements: Array<{ element: string | ReactNode; id: string }> = [];\n let lastIndex = 0;\n const nextId = () => elements.length;\n\n const regex = /\\{([a-zA-Z0-9-]+)\\}/g;\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n const [fullMatch, placeholderId] = match;\n const startIndex = match.index;\n\n // Add text before placeholder\n if (startIndex > lastIndex) {\n elements.push({\n element: text.slice(lastIndex, startIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n // Add the appropriate element\n if (isCorrectAnswer) {\n elements.push({\n element: renderCorrectAnswer(placeholderId),\n id: `${baseId}-answer-sheet-${nextId()}`,\n });\n } else {\n elements.push({\n element: renderStudentBadge(placeholderId),\n id: `${baseId}-student-${nextId()}`,\n });\n }\n\n lastIndex = match.index + fullMatch.length;\n }\n\n // Add remaining text\n if (lastIndex < text.length) {\n elements.push({\n element: text.slice(lastIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n return elements;\n };\n\n if (!additionalContent) {\n return (\n <div className=\"pt-2\">\n <Text size=\"md\" color=\"text-text-600\" weight=\"normal\">\n Nenhum conteúdo disponível para esta questão.\n </Text>\n </div>\n );\n }\n\n return (\n <div className=\"pt-2 space-y-4\">\n <div className=\"space-y-2\">\n <Text size=\"xs\" weight=\"normal\" color=\"text-text-500\">\n Resposta do aluno:\n </Text>\n <div className=\"p-3 bg-background-50 rounded-lg border border-border-100\">\n <Text\n as=\"div\"\n size=\"md\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderTextWithElements(cleanText, false).map((element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n ))}\n </Text>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <Text size=\"xs\" weight=\"normal\" color=\"text-text-500\">\n Gabarito:\n </Text>\n <div className=\"p-3 bg-background-50 rounded-lg border border-border-100\">\n <Text\n as=\"div\"\n size=\"md\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderTextWithElements(cleanText, true).map((element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n ))}\n </Text>\n </div>\n </div>\n </div>\n );\n};\n","import { getStatusStyles } from '../Quiz/QuizContent';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport { getStatusBadge } from '../../utils/questionRenderer/components';\nimport { HtmlMathRenderer } from '../HtmlMathRenderer';\nimport { TrueFalseEnum } from '../../enums/Quiz';\nimport { OptionStatus } from '../../enums/Options';\n\n/** One statement of a true/false question, already normalized for rendering. */\nexport interface TrueFalseStatement {\n id: string;\n /** Statement text; may contain HTML and LaTeX */\n statement: string;\n /** What the student marked. Null when the statement was left blank. */\n studentMark: TrueFalseEnum | null;\n /** Answer key: whether the statement is true. Null when unknown. */\n isTrue: boolean | null;\n}\n\nexport interface TrueFalseStatementListProps {\n readonly statements: readonly TrueFalseStatement[];\n /**\n * Whether the answer key may be revealed. False while the answer is still\n * pending evaluation or was never submitted.\n */\n readonly showCorrectness?: boolean;\n}\n\n/**\n * Read-only list of true/false statements with the student's mark and the\n * answer key.\n *\n * Shared by the activity correction modal and the simulation detail modal so\n * both read the same way. The two callers reach this from different payload\n * shapes — `QuestionResult.selectedOptions` in one, an option's `selectedValue`\n * in the other — and normalize into {@link TrueFalseStatement} before calling.\n *\n * @param props - Component props\n * @returns JSX element\n *\n * @example\n * ```tsx\n * <TrueFalseStatementList\n * statements={[{ id: '1', statement: 'A água ferve a 100°C', studentMark: TrueFalseEnum.VERDADEIRO, isTrue: true }]}\n * showCorrectness\n * />\n * ```\n */\nexport const TrueFalseStatementList = ({\n statements,\n showCorrectness = true,\n}: TrueFalseStatementListProps) => {\n // 97 = 'a' in ASCII\n const getLetterByIndex = (index: number) => String.fromCodePoint(97 + index);\n\n return (\n <div className=\"pt-2\">\n <div className=\"flex flex-col gap-3.5\">\n {statements.map((item, index) => {\n const hasAnswerKey = item.isTrue !== null;\n const hasAnswered = item.studentMark !== null;\n const studentMarkedTrue =\n item.studentMark === TrueFalseEnum.VERDADEIRO;\n\n const isStudentCorrect =\n hasAnswerKey && hasAnswered && studentMarkedTrue === item.isTrue;\n\n // Correctness is only shown once the answer key is known AND the\n // answer is no longer pending.\n const canShowCorrectness = showCorrectness && hasAnswerKey;\n const variantCorrect = isStudentCorrect\n ? OptionStatus.CORRECT\n : OptionStatus.INCORRECT;\n const correctAnswer = item.isTrue\n ? TrueFalseEnum.VERDADEIRO\n : TrueFalseEnum.FALSO;\n\n return (\n <section\n key={item.id || `statement-${index}`}\n className=\"flex flex-col gap-2\"\n >\n <div\n className={cn(\n 'flex flex-row justify-between items-center gap-2 p-2 rounded-md border',\n canShowCorrectness && hasAnswered\n ? getStatusStyles(variantCorrect)\n : ''\n )}\n >\n <Text as=\"span\" size=\"sm\" weight=\"normal\" color=\"text-text-900\">\n {getLetterByIndex(index).concat(') ')}\n <HtmlMathRenderer content={item.statement} inline />\n </Text>\n\n {canShowCorrectness && hasAnswered && (\n <div className=\"flex-shrink-0\">\n {getStatusBadge(variantCorrect)}\n </div>\n )}\n </div>\n\n {canShowCorrectness && (\n <span className=\"flex flex-row gap-2 items-center flex-wrap\">\n {hasAnswered ? (\n <>\n <Text size=\"2xs\" weight=\"normal\" color=\"text-text-800\">\n Resposta selecionada: {item.studentMark}\n </Text>\n {!isStudentCorrect && (\n <Text size=\"2xs\" weight=\"normal\" color=\"text-text-800\">\n | Resposta correta: {correctAnswer}\n </Text>\n )}\n </>\n ) : (\n <Text size=\"2xs\" weight=\"normal\" color=\"text-text-800\">\n Não respondida | Resposta correta: {correctAnswer}\n </Text>\n )}\n </span>\n )}\n </section>\n );\n })}\n </div>\n </div>\n );\n};\n\nexport default TrueFalseStatementList;\n","/**\n * Question status enum for student activity correction\n * Maps from ANSWER_STATUS to a simpler status for UI display\n */\nexport const QUESTION_STATUS = {\n CORRETA: 'CORRETA',\n INCORRETA: 'INCORRETA',\n EM_BRANCO: 'EM_BRANCO',\n /** Reserved for future use - pending teacher evaluation for essay questions */\n PENDENTE: 'PENDENTE',\n} as const;\n\nexport type QuestionStatus =\n (typeof QUESTION_STATUS)[keyof typeof QUESTION_STATUS];\n","import { StatusBadgeConfig } from '@/types/activityDetails';\nimport {\n ANSWER_STATUS,\n QUESTION_TYPE,\n} from '../../components/Quiz/useQuizStore';\nimport { QUESTION_STATUS, type QuestionStatus } from './constants';\nimport type { CorrectionQuestionData } from './types';\n\n/**\n * Returns whether the answer is correct, incorrect, or null based on the answer status.\n * @param answerStatus - Answer status from ANSWER_STATUS enum\n * @returns Returns true for correct, false for incorrect, or null if undefined.\n */\nexport const getIsCorrect = (answerStatus: ANSWER_STATUS): boolean | null => {\n if (answerStatus === ANSWER_STATUS.RESPOSTA_CORRETA) return true;\n if (answerStatus === ANSWER_STATUS.RESPOSTA_INCORRETA) return false;\n return null;\n};\n\n/**\n * Map ANSWER_STATUS from Quiz to QUESTION_STATUS for correction modal\n * @param answerStatus - Answer status from QuestionResult\n * @returns QuestionStatus for UI display\n */\nexport const mapAnswerStatusToQuestionStatus = (\n answerStatus: ANSWER_STATUS\n): QuestionStatus => {\n switch (answerStatus) {\n case ANSWER_STATUS.RESPOSTA_CORRETA:\n return QUESTION_STATUS.CORRETA;\n case ANSWER_STATUS.RESPOSTA_INCORRETA:\n return QUESTION_STATUS.INCORRETA;\n case ANSWER_STATUS.NAO_RESPONDIDO:\n return QUESTION_STATUS.EM_BRANCO;\n case ANSWER_STATUS.PENDENTE_AVALIACAO:\n return QUESTION_STATUS.PENDENTE;\n default:\n return QUESTION_STATUS.EM_BRANCO;\n }\n};\n\n/**\n * Get question status badge configuration\n * @param status - Question status\n * @returns Badge configuration with label and colors\n */\nexport const getQuestionStatusBadgeConfig = (status: QuestionStatus) => {\n const configs: Partial<Record<QuestionStatus, StatusBadgeConfig>> = {\n [QUESTION_STATUS.CORRETA]: {\n label: 'Correta',\n bgColor: 'bg-success-background',\n textColor: 'text-success-800',\n },\n [QUESTION_STATUS.INCORRETA]: {\n label: 'Incorreta',\n bgColor: 'bg-error-background',\n textColor: 'text-error-800',\n },\n [QUESTION_STATUS.EM_BRANCO]: {\n label: 'Em branco',\n bgColor: 'bg-gray-100',\n textColor: 'text-gray-600',\n },\n [QUESTION_STATUS.PENDENTE]: {\n label: 'Pendente',\n bgColor: 'bg-warning-background',\n textColor: 'text-warning-800',\n },\n };\n\n const defaultConfig = {\n label: 'Sem categoria',\n bgColor: 'bg-gray-100',\n textColor: 'text-gray-600',\n };\n\n return configs[status] ?? defaultConfig;\n};\n\n/**\n * Check if question can be auto-validated based on options\n * All question types can be auto-validated except DISSERTATIVA (essay questions)\n * @param questionData - Correction question data\n * @returns true if can auto-validate, false otherwise\n */\nexport const canAutoValidate = (\n questionData: CorrectionQuestionData\n): boolean => {\n const { result } = questionData;\n\n // Only dissertative questions require manual evaluation\n if (result.questionType === QUESTION_TYPE.DISSERTATIVA) {\n return false;\n }\n\n return true;\n};\n\ntype Option = {\n id: string;\n option: string;\n isCorrect: boolean;\n};\n\n/**\n * Type predicate to check if an option has isCorrect defined\n */\nconst hasIsCorrect = (op: {\n id: string;\n option: string;\n isCorrect?: boolean | null;\n}): op is Option => {\n return op.isCorrect != null;\n};\n\n/**\n * Validate alternativa (single choice) question\n * Must select exactly one correct option\n */\nconst validateAlternativa = (\n selected: Set<string>,\n options: Option[]\n): ANSWER_STATUS => {\n if (selected.size !== 1) return ANSWER_STATUS.RESPOSTA_INCORRETA;\n\n const [selectedId] = selected;\n return options.find((o) => o.id === selectedId)?.isCorrect\n ? ANSWER_STATUS.RESPOSTA_CORRETA\n : ANSWER_STATUS.RESPOSTA_INCORRETA;\n};\n\n/**\n * Validate multipla escolha (multiple choice) question\n * Each option must match its correct state: selected if correct, not selected if incorrect\n */\nconst validateMultiplaEscolha = (\n selected: Set<string>,\n options: Option[]\n): ANSWER_STATUS => {\n const allMatch = options.every((op) => selected.has(op.id) === op.isCorrect);\n\n return allMatch\n ? ANSWER_STATUS.RESPOSTA_CORRETA\n : ANSWER_STATUS.RESPOSTA_INCORRETA;\n};\n\n/**\n * Validate verdadeiro/falso (true/false) question\n * Each statement is evaluated individually: if statement is true, must be selected;\n * if statement is false, must not be selected\n */\nconst validateVerdadeiroFalso = validateMultiplaEscolha;\n\nconst validators: Partial<\n Record<QUESTION_TYPE, (s: Set<string>, o: Option[]) => ANSWER_STATUS>\n> = {\n [QUESTION_TYPE.ALTERNATIVA]: validateAlternativa,\n [QUESTION_TYPE.MULTIPLA_ESCOLHA]: validateMultiplaEscolha,\n [QUESTION_TYPE.VERDADEIRO_FALSO]: validateVerdadeiroFalso,\n};\n\n/**\n * Auto-validate question based on selected options vs correct options\n * @param questionData - Correction question data\n * @returns ANSWER_STATUS if can determine, null otherwise\n */\nexport const autoValidateQuestion = (\n questionData: CorrectionQuestionData\n): ANSWER_STATUS | null => {\n const { result } = questionData;\n\n if (!canAutoValidate(questionData) || !result.options) return null;\n\n // Filter options to only include those with isCorrect defined\n const validOptions = result.options.filter(hasIsCorrect);\n if (validOptions.length === 0) return null;\n\n const selected = new Set(\n result.selectedOptions?.map((o) => o.optionId) ?? []\n );\n\n if (selected.size === 0) return ANSWER_STATUS.NAO_RESPONDIDO;\n\n const validator = validators[result.questionType];\n if (!validator) return null;\n\n return validator(selected, validOptions);\n};\n\n/**\n * Get question status from CorrectionQuestionData\n * Maps the result's answerStatus to QuestionStatus\n * If status is PENDENTE_AVALIACAO but can auto-validate, calculates status automatically\n * @param questionData - Correction question data\n * @returns QuestionStatus for UI display\n */\nexport const getQuestionStatusFromData = (\n questionData: CorrectionQuestionData\n): QuestionStatus => {\n const { result } = questionData;\n\n // If pending evaluation, try to auto-validate\n if (\n result.answerStatus === ANSWER_STATUS.PENDENTE_AVALIACAO &&\n canAutoValidate(questionData)\n ) {\n const autoValidatedStatus = autoValidateQuestion(questionData);\n if (autoValidatedStatus !== null) {\n return mapAnswerStatusToQuestionStatus(autoValidatedStatus);\n }\n }\n\n return mapAnswerStatusToQuestionStatus(result.answerStatus);\n};\n","import type { SimulationContentSummary } from '../../components/shared/SimulationSummaryCards';\nimport { QUESTION_STATUS } from './constants';\nimport type { CorrectionQuestionData } from './types';\nimport { getQuestionStatusFromData } from './utils';\n\n/**\n * Hit rate above which a lone subtema reads as a result rather than a gap.\n *\n * The same 50% the report's `findBestAndWorstTopics` uses, so a student's\n * correction modal and their row in the Desempenho report never disagree on\n * which side of the line one subtema falls.\n */\nconst PERFORMANCE_THRESHOLD = 50;\n\n/** Running tally of one subtema across the student's answers. */\ninterface SubtopicTally {\n name: string;\n answered: number;\n correct: number;\n}\n\n/**\n * The subtema a question belongs to, or null when it hangs outside the\n * knowledge matrix (the API sends `{ id: '', name: '' }` for those).\n */\nfunction getSubtopic(\n questionData: CorrectionQuestionData\n): { id: string; name: string } | null {\n const subtopic = questionData.question.knowledgeMatrix?.[0]?.subtopic;\n return subtopic?.id && subtopic.name ? subtopic : null;\n}\n\n/**\n * Best and worst subtema of one student's answers to an activity.\n *\n * The correction endpoint answers question by question, so the two cards are\n * derived here rather than sent: answers are grouped by the subtema of their\n * knowledge matrix and ranked by hit rate.\n *\n * Only answered questions count. A blank or still-ungraded question says\n * nothing about the subtema, and counting it as a miss would name a \"maior\n * dificuldade\" the student never attempted.\n *\n * @param questions - The student's questions, as the modal lists them\n * @returns The best and the worst subtema; either is null when there is none\n *\n * @example\n * ```typescript\n * findBestAndWorstSubtopics(questions);\n * // { best: { contentName: 'Cinemática' }, worst: { contentName: 'Óptica' } }\n * ```\n */\nexport function findBestAndWorstSubtopics(\n questions: readonly CorrectionQuestionData[]\n): {\n best: SimulationContentSummary | null;\n worst: SimulationContentSummary | null;\n} {\n const tallies = new Map<string, SubtopicTally>();\n\n for (const questionData of questions) {\n const subtopic = getSubtopic(questionData);\n if (!subtopic) continue;\n\n const status = getQuestionStatusFromData(questionData);\n if (\n status !== QUESTION_STATUS.CORRETA &&\n status !== QUESTION_STATUS.INCORRETA\n ) {\n continue;\n }\n\n const tally = tallies.get(subtopic.id) ?? {\n name: subtopic.name,\n answered: 0,\n correct: 0,\n };\n tally.answered += 1;\n if (status === QUESTION_STATUS.CORRETA) tally.correct += 1;\n tallies.set(subtopic.id, tally);\n }\n\n // Ties keep the order the questions came in, which is the order of the\n // activity — `sort` is stable, so no extra key is needed for that.\n const ranked = [...tallies.values()]\n .map((tally) => ({\n contentName: tally.name,\n rate: (tally.correct / tally.answered) * 100,\n }))\n .sort((a, b) => b.rate - a.rate);\n\n if (ranked.length === 0) return { best: null, worst: null };\n\n if (ranked.length === 1) {\n const [only] = ranked;\n const summary = { contentName: only.contentName };\n return only.rate >= PERFORMANCE_THRESHOLD\n ? { best: summary, worst: null }\n : { best: null, worst: summary };\n }\n\n // The list is sorted by hit rate and has at least two entries by this point,\n // so the ends of it are the two cards.\n const [best] = ranked;\n const [worst] = ranked.slice(-1);\n\n return {\n best: { contentName: best.contentName },\n worst: { contentName: worst.contentName },\n };\n}\n","import type { SimulatedPerformanceTag } from '../../components/SimulatedStudentSimulationsModal/types';\n\n/**\n * Percentage floors of each band, as the reports classify a student.\n *\n * The absolute scale (`performanceMode=absolute`), not one relative to the\n * class: this modal describes a single student's activity, so there is no\n * cohort to compare them against.\n */\nconst BAND_FLOORS = {\n HIGHLIGHT: 90,\n ABOVE_AVERAGE: 70,\n BELOW_AVERAGE: 40,\n} as const;\n\n/**\n * The band the student's grade falls in, for the badge of the correction modal.\n *\n * The correction endpoint sends the stored grade (0-10) and no tag, so the\n * badge is derived here with the bands the Desempenho report uses, on the\n * percentage scale those bands are written in.\n *\n * @param score - Grade out of ten; null while the activity is ungraded\n * @returns The band, or null when there is no grade to classify\n *\n * @example\n * ```typescript\n * getCorrectionPerformanceTag(9.2); // 'HIGHLIGHT'\n * getCorrectionPerformanceTag(null); // null\n * ```\n */\nexport function getCorrectionPerformanceTag(\n score: number | null | undefined\n): SimulatedPerformanceTag | null {\n if (score === null || score === undefined || !Number.isFinite(score)) {\n return null;\n }\n\n const percentage = score * 10;\n if (percentage >= BAND_FLOORS.HIGHLIGHT) return 'HIGHLIGHT';\n if (percentage >= BAND_FLOORS.ABOVE_AVERAGE) return 'ABOVE_AVERAGE';\n if (percentage >= BAND_FLOORS.BELOW_AVERAGE) return 'BELOW_AVERAGE';\n return 'ATTENTION_POINT';\n}\n","import type { Question } from '../../components/Quiz/useQuizStore';\nimport { QUESTION_TYPE } from '../../components/Quiz/useQuizStore';\nimport type {\n QuestionsAnswersByStudentResponse,\n StudentActivityCorrectionData,\n CorrectionQuestionData,\n QuestionCorrection,\n} from './types';\nimport { getIsCorrect } from './utils';\n\n/**\n * Build Question object from answer data\n * @param answer - Answer data from QuestionResult\n * @returns Question object in Quiz format\n */\nconst buildQuestionFromAnswer = (\n answer: QuestionsAnswersByStudentResponse['data']['answers'][number]\n): Question => {\n return {\n id: answer.questionId,\n statement: answer.statement,\n questionType: answer.questionType,\n difficultyLevel: answer.difficultyLevel,\n description: '',\n examBoard: null,\n examYear: null,\n solutionExplanation: answer.solutionExplanation || null,\n additionalContent: answer.additionalContent || null,\n answer: null,\n answerStatus: answer.answerStatus,\n options: answer.options || [],\n knowledgeMatrix: answer.knowledgeMatrix.map((matrix) => ({\n areaKnowledge: matrix.areaKnowledge || {\n id: '',\n name: '',\n },\n subject: matrix.subject || {\n id: '',\n name: '',\n color: '',\n icon: '',\n },\n topic: matrix.topic || {\n id: '',\n name: '',\n },\n subtopic: matrix.subtopic || {\n id: '',\n name: '',\n },\n content: matrix.content || {\n id: '',\n name: '',\n },\n })),\n correctOptionIds:\n answer.options?.filter((opt) => opt.isCorrect).map((opt) => opt.id) || [],\n };\n};\n\n/**\n * Build correction data for a question\n *\n * Built for every question type, not just DISSERTATIVA: a teacher can comment on\n * any question. `isCorrect` stays null outside DISSERTATIVA because objective\n * questions are graded automatically and must not be re-graded by hand.\n *\n * The AI fields are carried through untouched rather than folded into\n * `teacherFeedback`. The modal needs both to do its job: the AI's text to\n * prefill the field the teacher edits, and the source to decide which tag to\n * show and whether saving turns an `IA` correction into an `IA + PROFESSOR` one.\n *\n * @param answer - Answer data from QuestionResult\n * @returns QuestionCorrection with the teacher's grade and/or comment\n */\nconst buildQuestionCorrection = (\n answer: QuestionsAnswersByStudentResponse['data']['answers'][number]\n): QuestionCorrection => ({\n isCorrect:\n answer.questionType === QUESTION_TYPE.DISSERTATIVA\n ? getIsCorrect(answer.answerStatus)\n : null,\n teacherFeedback: answer.teacherFeedback || '',\n aiFeedback: answer.aiFeedback ?? null,\n aiIsCorrect: answer.aiIsCorrect ?? null,\n aiCorrectionStatus: answer.aiCorrectionStatus ?? null,\n correctionSource: answer.correctionSource ?? null,\n});\n\n/**\n * Convert a single answer to CorrectionQuestionData format\n * @param answer - Answer data from QuestionResult\n * @param index - Index of the answer (0-based)\n * @returns CorrectionQuestionData formatted for the modal\n */\nconst convertAnswerToQuestionData = (\n answer: QuestionsAnswersByStudentResponse['data']['answers'][number],\n index: number\n): CorrectionQuestionData => {\n const question = buildQuestionFromAnswer(answer);\n const correction = buildQuestionCorrection(answer);\n\n return {\n question,\n result: answer,\n questionNumber: index + 1,\n correction,\n };\n};\n\n/**\n * Convert QuestionResult from API to StudentActivityCorrectionData\n * This function transforms the API response into the format expected by CorrectActivityModal\n *\n * `statistics.score` from the answers endpoint is a 0-100 percentage recomputed\n * from the per-question rows, while the students table shows the stored 0-10\n * grade. Callers that already hold the stored grade pass it as `score` so the\n * modal and the table never disagree; `null` is an explicit \"no grade yet\".\n *\n * @param apiResponse - API response with answers and statistics\n * @param studentId - Student ID\n * @param studentName - Student name\n * @param observation - Optional teacher observation\n * @param attachment - Optional attachment URL\n * @param score - Optional stored grade (0-10) that takes precedence over\n * `statistics.score`; `null` means the student has no grade yet\n * @returns StudentActivityCorrectionData formatted for the modal\n */\nexport const convertApiResponseToCorrectionData = (\n apiResponse: QuestionsAnswersByStudentResponse,\n studentId: string,\n studentName: string,\n observation?: string,\n attachment?: string,\n score?: number | null\n): StudentActivityCorrectionData => {\n const { answers, statistics } = apiResponse.data;\n\n // Calculate counts from statistics\n const correctCount = statistics.correctAnswers;\n const incorrectCount = statistics.incorrectAnswers;\n const blankCount = statistics.totalAnswered - correctCount - incorrectCount;\n\n // Convert answers to CorrectionQuestionData format\n const questions: CorrectionQuestionData[] = answers.map((answer, index) =>\n convertAnswerToQuestionData(answer, index)\n );\n\n return {\n studentId,\n studentName,\n score: score === undefined ? (statistics.score ?? null) : score,\n correctCount,\n incorrectCount,\n blankCount,\n questions,\n observation,\n attachment,\n };\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport type { ChangeEvent } from 'react';\nimport { PaperclipIcon } from '@phosphor-icons/react/dist/csr/Paperclip';\nimport { XIcon } from '@phosphor-icons/react/dist/csr/X';\nimport Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport TextArea from '../TextArea/TextArea';\nimport { CardAccordation } from '../Accordation';\nimport { SkeletonCard } from '../Skeleton/Skeleton';\nimport { QuestionCommentField } from '../shared/QuestionCommentField';\nimport {\n TrueFalseStatementList,\n type TrueFalseStatement,\n} from '../shared/TrueFalseStatementList';\nimport { ImageAnswerView } from '../shared/ImageAnswerView';\nimport { DEFAULT_IMAGE_TOLERANCE } from '../../utils/image/imageAnswer.utils';\nimport { AlternativesList } from '../Alternative/Alternative';\nimport { HtmlMathRenderer } from '../HtmlMathRenderer';\nimport { OptionStatus } from '../../enums/Options';\nimport { QUESTION_TYPE } from '../Quiz/useQuizStore';\nimport {\n getQuestionStatusBadgeConfig,\n QUESTION_STATUS,\n type QuestionStatus,\n} from '../../utils/studentActivityCorrection';\nimport {\n buildSimulationMeta,\n ContentCards,\n DataCard,\n SectionTitle,\n SimulationCardShell,\n SimulationStatCards,\n StudentSummaryHeader,\n} from '../shared/SimulationSummaryCards';\nimport { cn } from '../../utils/utils';\nimport { formatQuestionDuration } from '../../utils/questionDuration';\nimport { formatTimeSpent } from '../../utils/activityDetailsUtils';\nimport type { BaseApiClient } from '../../types/api';\nimport { createUseSimulations } from '../../hooks/useSimulations';\nimport {\n useSimulationCardDetails,\n type SimulationCardHandlers,\n type SimulationDetailState,\n type SimulationNoteState,\n} from '../../hooks/useSimulationCardDetails';\nimport type {\n SimulationsListData,\n SimulationDetailQuestion,\n StudentSimulationItem,\n NoteData,\n} from '../../types/simulations';\n\nexport interface SimulationsDetailModalProps {\n /** API client used to fetch the student's simulations */\n readonly api: BaseApiClient;\n readonly isOpen: boolean;\n readonly onClose: () => void;\n /** The student whose simulations are shown (null closes the modal) */\n readonly student: { userInstitutionId: string; name: string } | null;\n}\n\n/** Map the simulation question status to the shared correction status. */\nconst QUESTION_STATUS_MAP: Record<\n SimulationDetailQuestion['status'],\n QuestionStatus\n> = {\n CORRECT: QUESTION_STATUS.CORRETA,\n INCORRECT: QUESTION_STATUS.INCORRETA,\n BLANK: QUESTION_STATUS.EM_BRANCO,\n PENDING: QUESTION_STATUS.PENDENTE,\n};\n\n/** Label of the inner accordion holding the student's answer. */\nfunction getAnswerAccordionTitle(questionType: string): string {\n if (questionType === QUESTION_TYPE.DISSERTATIVA) {\n return 'Resposta do aluno';\n }\n if (questionType === QUESTION_TYPE.VERDADEIRO_FALSO) {\n return 'Afirmações';\n }\n if (questionType === QUESTION_TYPE.IMAGEM) {\n return 'Imagem';\n }\n return 'Alternativas';\n}\n\n// ---------------------------------------------------------------------------\n// Level 2 — Question (reuses the shared alternatives renderer + status badge)\n// ---------------------------------------------------------------------------\n\nexport interface SimulationQuestionItemProps {\n readonly question: SimulationDetailQuestion;\n /** Zero-based position in the simulation; shown as \"Questão N\". */\n readonly index: number;\n /**\n * Persist the teacher comment on this question; an empty string clears it.\n * Left out, the question is read-only and has no comment field — a report\n * that shows the answers but has nobody to write to (the Momento ENEM\n * report of the gestor app).\n */\n readonly onSaveComment?: (comment: string) => Promise<void>;\n}\n\n/**\n * One question of a student's simulation: an accordion with the status badge\n * in its header and, inside, the statement, the student's answer (alternatives,\n * true/false marks, essay text or image click, by question type) and, when\n * the caller can save it, the teacher comment field.\n *\n * Exported so consumers that list a student's simulations in their own layout\n * (the Desempenho report of the teacher app, the Momento ENEM report of the\n * gestor app) render questions exactly as the Simulados page does.\n */\nexport function SimulationQuestionItem({\n question,\n index,\n onSaveComment,\n}: SimulationQuestionItemProps) {\n const badge = getQuestionStatusBadgeConfig(\n QUESTION_STATUS_MAP[question.status]\n );\n\n // The subject only joins the label when the question has one mapped in the\n // knowledge matrix; without it the row falls back to a plain \"Questão N\".\n const label = [`Questão ${index + 1}`, question.subject]\n .filter(Boolean)\n .join(' - ');\n\n // Time spent sits on the right, next to the badge, and not glued to the\n // label: it is a measurement, read down the column against the other\n // questions, not part of the question's name. Null when it was never\n // measured, and then the row shows nothing rather than `00:00:00`.\n const duration = formatQuestionDuration(question.timeSpent);\n\n const alternatives = question.options.map((option) => {\n let status: OptionStatus;\n if (option.isCorrect) {\n status = OptionStatus.CORRECT;\n } else if (option.isSelected) {\n status = OptionStatus.INCORRECT;\n } else {\n status = OptionStatus.NEUTRAL;\n }\n return { label: option.option, value: option.id, status };\n });\n\n // An essay has no alternatives to show — it has the text the student wrote.\n // Rendering the \"Alternativas\" accordion for it produced an empty box and hid\n // the answer entirely, leaving the teacher to comment on nothing.\n const isEssay = question.questionType === QUESTION_TYPE.DISSERTATIVA;\n\n // True/false never writes `option_id`, so `isSelected` is always false and the\n // student's marks live in each option's `selectedValue`. Sending it through\n // the alternatives branch showed the answer key as if it were the student's\n // answer, and no mark at all.\n const isTrueFalse = question.questionType === QUESTION_TYPE.VERDADEIRO_FALSO;\n const trueFalseStatements: TrueFalseStatement[] = question.options.map(\n (option) => ({\n id: option.id,\n statement: option.option,\n studentMark: option.selectedValue ?? null,\n isTrue: option.isCorrect,\n })\n );\n\n // IMAGEM has no alternatives either: the answer is a point on an image. The\n // alternatives branch rendered the answer key's raw JSON as an option label,\n // painted green, and never showed where the student clicked.\n const isImage = question.questionType === QUESTION_TYPE.IMAGEM;\n\n /**\n * Render the student's answer, shaped by the question type.\n */\n const renderAnswerArea = () => {\n if (isImage) {\n return (\n <ImageAnswerView\n imageUrl={question.additionalContent ?? ''}\n correctPoint={question.correctPoint}\n studentPoint={\n question.imageAnswer\n ? {\n x: question.imageAnswer.coordinateX,\n y: question.imageAnswer.coordinateY,\n }\n : null\n }\n toleranceRadius={question.imageTolerance ?? DEFAULT_IMAGE_TOLERANCE}\n />\n );\n }\n\n if (isEssay) {\n return (\n <div className=\"rounded-lg border border-border-100 bg-background-50 p-3\">\n {question.answer ? (\n <HtmlMathRenderer\n content={question.answer}\n className=\"text-sm text-text-800\"\n />\n ) : (\n <Text size=\"sm\" className=\"text-text-600\">\n Nenhuma resposta fornecida\n </Text>\n )}\n </div>\n );\n }\n\n if (isTrueFalse) {\n return <TrueFalseStatementList statements={trueFalseStatements} />;\n }\n\n return (\n <AlternativesList\n mode=\"readonly\"\n layout=\"compact\"\n name={`question-${question.questionId}`}\n alternatives={alternatives}\n selectedValue={question.selectedOptionId ?? ''}\n />\n );\n };\n\n return (\n <CardAccordation\n value={question.questionId}\n trigger={\n <div className=\"flex flex-1 items-center justify-between gap-3 py-3\">\n <Text size=\"sm\" weight=\"bold\" className=\"min-w-0 text-text-950\">\n {label}\n </Text>\n <div className=\"flex shrink-0 items-center gap-2\">\n {duration && (\n <Text\n size=\"xs\"\n className=\"whitespace-nowrap tabular-nums text-text-600\"\n >\n {duration}\n </Text>\n )}\n <span\n className={cn(\n 'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium',\n badge.bgColor,\n badge.textColor\n )}\n >\n {badge.label}\n </span>\n </div>\n </div>\n }\n contentClassName=\"px-3 pb-3\"\n >\n <div className=\"flex flex-col gap-3\">\n <HtmlMathRenderer\n content={question.statement}\n className=\"text-sm text-text-800\"\n />\n <CardAccordation\n value={`${question.questionId}-answer`}\n trigger={\n <div className=\"flex-1 py-2\">\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-950\">\n {getAnswerAccordionTitle(question.questionType)}\n </Text>\n </div>\n }\n contentClassName=\"px-3 pb-3\"\n >\n {renderAnswerArea()}\n </CardAccordation>\n {onSaveComment && (\n <QuestionCommentField\n value={question.teacherComment ?? ''}\n onSave={onSaveComment}\n />\n )}\n </div>\n </CardAccordation>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Note (\"Observação\")\n// ---------------------------------------------------------------------------\n\nexport interface SimulationNoteRowProps {\n /** Current observation, null when the teacher never wrote one. */\n readonly note: NoteData | null;\n readonly loading: boolean;\n /**\n * Message of a failed load. While it is set the row offers a retry instead\n * of the editor: writing over a note that could not be read would replace\n * it, and this save has no version to protect it.\n */\n readonly loadError?: string | null;\n /** Load the observation again; required whenever `loadError` can be set. */\n readonly onRetry?: () => void;\n /** Placeholder of the editor; defaults to the simulado wording. */\n readonly placeholder?: string;\n /**\n * Persist the observation. `text` is already trimmed and non-empty; `file`\n * is a newly chosen attachment still to be uploaded (null when none), and\n * `existingAttachment` is the URL of the saved file the teacher kept — null\n * when there was none, when it was removed, or when a new file replaces it.\n */\n readonly onSave: (\n text: string,\n file: File | null,\n existingAttachment: string | null\n ) => Promise<void>;\n}\n\n/**\n * Human label of an attachment URL: its file name, or a generic word when the\n * URL carries none.\n */\nfunction getAttachmentLabel(url: string): string {\n const lastSegment = url.split('?')[0].split('/').pop() ?? '';\n try {\n return decodeURIComponent(lastSegment) || 'Anexo';\n } catch {\n return lastSegment || 'Anexo';\n }\n}\n\n/**\n * Grey pill naming an attached file. Links to the file when `href` is given\n * and offers a remove button when `onRemove` is given — the same chip the\n * activity correction modal uses for its observation attachment.\n */\nfunction AttachmentChip({\n label,\n href,\n onRemove,\n}: {\n readonly label: string;\n readonly href?: string;\n readonly onRemove?: () => void;\n}) {\n const content = (\n <>\n <PaperclipIcon size={18} className=\"shrink-0 text-text-800\" />\n <span className=\"truncate text-md font-medium text-text-800\">\n {label}\n </span>\n </>\n );\n\n return (\n <div className=\"flex h-10 min-w-0 max-w-[220px] items-center gap-2 rounded-full bg-secondary-500 px-5\">\n {href ? (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex min-w-0 items-center gap-2 hover:underline\"\n >\n {content}\n </a>\n ) : (\n content\n )}\n {onRemove && (\n <button\n type=\"button\"\n onClick={onRemove}\n className=\"shrink-0 text-text-700 hover:text-text-950\"\n aria-label={`Remover ${label}`}\n >\n <XIcon size={18} />\n </button>\n )}\n </div>\n );\n}\n\n/**\n * The simulation-wide teacher observation: a row with the saved text, the\n * attached file (if any) and an \"Incluir\"/\"Editar\" button that swaps into a\n * textarea with \"Anexar\" and \"Salvar\" — the same flow as the observation of\n * the activity correction modal.\n *\n * Exported for the same reason as {@link SimulationQuestionItem}.\n */\nexport function SimulationNoteRow({\n note,\n loading,\n loadError,\n onRetry,\n placeholder = 'Escreva uma observação para este simulado',\n onSave,\n}: SimulationNoteRowProps) {\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState('');\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n /** File chosen in this editing session, not uploaded yet. */\n const [pendingFile, setPendingFile] = useState<File | null>(null);\n /** Saved attachment the teacher is keeping; null once removed. */\n const [keptAttachment, setKeptAttachment] = useState<string | null>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const savedAttachment = note?.attachment ?? null;\n\n const startEditing = () => {\n setDraft(note?.note ?? '');\n setPendingFile(null);\n setKeptAttachment(savedAttachment);\n setError(null);\n setEditing(true);\n };\n\n const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0] ?? null;\n if (file) setPendingFile(file);\n // Reset so the same file can be picked again after being removed.\n event.target.value = '';\n };\n\n const handleSave = async () => {\n if (!draft.trim()) return;\n setSaving(true);\n setError(null);\n try {\n await onSave(\n draft.trim(),\n pendingFile,\n pendingFile ? null : keptAttachment\n );\n setEditing(false);\n } catch {\n // Keep the editing UI open (draft preserved) and surface the failure.\n setError('Erro ao salvar a observação. Tente novamente.');\n } finally {\n setSaving(false);\n }\n };\n\n /** Left side of the footer: the chosen/kept file, or the \"Anexar\" button. */\n const renderAttachmentControl = () => {\n if (pendingFile) {\n return (\n <AttachmentChip\n label={pendingFile.name}\n onRemove={() => setPendingFile(null)}\n />\n );\n }\n if (keptAttachment) {\n return (\n <AttachmentChip\n label={getAttachmentLabel(keptAttachment)}\n href={keptAttachment}\n onRemove={() => setKeptAttachment(null)}\n />\n );\n }\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"medium\"\n onClick={() => fileInputRef.current?.click()}\n className=\"flex items-center gap-2\"\n >\n <PaperclipIcon size={18} />\n Anexar\n </Button>\n );\n };\n\n if (loading) {\n // O `SkeletonCard` já é `role=\"status\"` e anuncia o carregamento.\n return <SkeletonCard className=\"h-14\" />;\n }\n\n if (loadError) {\n return (\n <div className=\"flex items-center justify-between gap-2 rounded-lg border border-border-200 bg-background p-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <Text size=\"md\" weight=\"bold\" className=\"text-text-950\">\n Observação\n </Text>\n {/* `role=\"alert\"`: a falha precisa ser anunciada na hora. */}\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {loadError}\n </Text>\n </div>\n {onRetry && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"medium\"\n onClick={onRetry}\n className=\"shrink-0\"\n >\n Tentar novamente\n </Button>\n )}\n </div>\n );\n }\n\n if (editing) {\n return (\n <div className=\"flex flex-col gap-4 rounded-lg border border-border-200 bg-background p-4\">\n <Text size=\"md\" weight=\"bold\" className=\"text-text-950\">\n Observação\n </Text>\n <TextArea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder={placeholder}\n rows={3}\n />\n {error && (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {error}\n </Text>\n )}\n {/* Only images: the pre-signed upload endpoint accepts nothing else. */}\n <input\n type=\"file\"\n ref={fileInputRef}\n className=\"hidden\"\n accept=\"image/*\"\n onChange={handleFileChange}\n aria-label=\"Selecionar arquivo\"\n />\n <div className=\"flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between\">\n {renderAttachmentControl()}\n <Button\n type=\"button\"\n variant=\"solid\"\n size=\"medium\"\n onClick={handleSave}\n disabled={saving || !draft.trim()}\n >\n Salvar\n </Button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-center justify-between gap-2 rounded-lg border border-border-200 bg-background p-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <Text size=\"md\" weight=\"bold\" className=\"text-text-950\">\n Observação\n </Text>\n {note?.note && (\n <Text size=\"sm\" className=\"truncate text-text-700\">\n {note.note}\n </Text>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {savedAttachment && (\n <AttachmentChip\n label={getAttachmentLabel(savedAttachment)}\n href={savedAttachment}\n />\n )}\n <Button\n type=\"button\"\n variant=\"solid\"\n size=\"medium\"\n onClick={startEditing}\n >\n {note?.note ? 'Editar' : 'Incluir'}\n </Button>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Level 1 — Simulation\n// ---------------------------------------------------------------------------\n\n/**\n * Observation and question list of one simulado, loaded when its card is\n * first expanded.\n */\nexport function SimulationAnswers({\n detail,\n note,\n onRetryNote,\n notePlaceholder,\n onSaveNote,\n onSaveQuestionComment,\n}: {\n readonly detail: SimulationDetailState | undefined;\n readonly note: SimulationNoteState | undefined;\n /** Load the observation again after a failed request */\n readonly onRetryNote: () => void;\n /** Placeholder of the observation editor; see `SimulationNoteRowProps` */\n readonly notePlaceholder?: string;\n readonly onSaveNote: SimulationNoteRowProps['onSave'];\n readonly onSaveQuestionComment: (\n questionId: string,\n comment: string\n ) => Promise<void>;\n}) {\n if (!detail || detail.loading) {\n // O `SkeletonCard` já é `role=\"status\"` e anuncia o carregamento.\n return <SkeletonCard className=\"h-40\" />;\n }\n\n if (detail.error) {\n return (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {detail.error}\n </Text>\n );\n }\n\n if (!detail.data) return null;\n\n return (\n <>\n <SimulationNoteRow\n note={note?.data ?? null}\n loading={note?.loading ?? false}\n loadError={note?.error ?? null}\n onRetry={onRetryNote}\n placeholder={notePlaceholder}\n onSave={onSaveNote}\n />\n\n <div className=\"flex flex-col gap-2 pt-2\">\n <Text as=\"h4\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Respostas\n </Text>\n {detail.data.questions.map((question, qIndex) => (\n <SimulationQuestionItem\n key={question.questionId}\n question={question}\n index={qIndex}\n onSaveComment={(comment) =>\n onSaveQuestionComment(question.questionId, comment)\n }\n />\n ))}\n </div>\n </>\n );\n}\n\nfunction SimulationItem({\n simulation,\n index,\n expanded,\n onToggle,\n detail,\n note,\n onRetryNote,\n onSaveNote,\n onSaveQuestionComment,\n}: SimulationCardHandlers & {\n readonly simulation: StudentSimulationItem;\n readonly index: number;\n readonly expanded: boolean;\n readonly onToggle: () => void;\n}) {\n const title = simulation.title?.trim()\n ? simulation.title.trim()\n : `Simulado ${index + 1}`;\n const meta = buildSimulationMeta(simulation);\n\n return (\n <SimulationCardShell\n value={simulation.id}\n title={title}\n meta={meta}\n correct={simulation.correctCount}\n totalQuestions={simulation.totalQuestions}\n expanded={expanded}\n onToggle={onToggle}\n >\n <SimulationStatCards\n score={simulation.score}\n correct={simulation.correctCount}\n incorrect={simulation.incorrectCount}\n blank={simulation.blankCount}\n />\n {/* Essays awaiting grading used to be counted as blank. */}\n {detail?.data && detail.data.counts.pending > 0 && (\n <Text size=\"sm\" className=\"text-text-600\">\n {`${detail.data.counts.pending} ${detail.data.counts.pending === 1 ? 'questão dissertativa aguarda' : 'questões dissertativas aguardam'} correção`}\n </Text>\n )}\n <ContentCards\n best={simulation.bestContent ?? null}\n worst={simulation.worstContent ?? null}\n />\n {expanded && (\n <SimulationAnswers\n detail={detail}\n note={note}\n onRetryNote={onRetryNote}\n onSaveNote={onSaveNote}\n onSaveQuestionComment={onSaveQuestionComment}\n />\n )}\n </SimulationCardShell>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Modal\n// ---------------------------------------------------------------------------\n\n/**\n * Modal that shows a single student's answered simulations as a nested\n * accordion (simulation -> question -> options), lazily loading each\n * simulation's detail when it is expanded. Reuses the shared StatCard and\n * AlternativesList components so the detail matches the activity correction UI.\n */\nexport function SimulationsDetailModal({\n api,\n isOpen,\n onClose,\n student,\n}: SimulationsDetailModalProps) {\n const useSimulations = useMemo(() => createUseSimulations(api), [api]);\n const { fetchStudentSimulations } = useSimulations();\n\n const [list, setList] = useState<SimulationsListData | null>(null);\n const [listLoading, setListLoading] = useState(false);\n const [listError, setListError] = useState<string | null>(null);\n\n const {\n expandedId,\n details,\n notes,\n toggle,\n retryNote,\n makeSaveNote,\n makeSaveQuestionComment,\n } = useSimulationCardDetails({\n api,\n userInstitutionId: student?.userInstitutionId ?? null,\n isOpen,\n });\n\n useEffect(() => {\n if (!isOpen || !student) return;\n\n setList(null);\n setListLoading(true);\n setListError(null);\n\n let active = true;\n fetchStudentSimulations(student.userInstitutionId)\n .then((data) => {\n if (active) setList(data);\n })\n .catch(() => {\n if (active) setListError('Erro ao carregar os simulados do estudante');\n })\n .finally(() => {\n if (active) setListLoading(false);\n });\n\n return () => {\n active = false;\n };\n }, [isOpen, student, fetchStudentSimulations]);\n\n return (\n <Modal isOpen={isOpen} onClose={onClose} title=\"Simulados\" size=\"xl\">\n {student && (\n // Região nomeada. Sem `tabIndex`: os navegadores atuais já tornam\n // focável pelo teclado um contêiner rolável sem controle focável.\n <section\n className=\"flex max-h-[70vh] flex-col gap-6 overflow-y-auto pr-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-950\"\n aria-label={`Simulados de ${student.name}`}\n >\n <StudentSummaryHeader\n name={student.name}\n location={[\n list?.student.school,\n list?.student.class,\n list?.student.schoolYear,\n ]}\n />\n\n {listLoading && (\n <>\n <SkeletonCard className=\"h-20\" />\n <SkeletonCard className=\"h-40\" />\n </>\n )}\n {listError && (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {listError}\n </Text>\n )}\n\n {list && (\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>Dados de simulados</SectionTitle>\n <div className=\"flex flex-col gap-2 md:flex-row\">\n <DataCard\n label=\"Simulados realizados\"\n value={String(list.student.simulationsAnswered)}\n />\n {list.student.totalTimeSeconds !== undefined && (\n <DataCard\n label=\"Tempo total\"\n value={formatTimeSpent(list.student.totalTimeSeconds)}\n />\n )}\n </div>\n </section>\n )}\n\n {list && (\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>Simulados realizados</SectionTitle>\n {list.simulations.data.length === 0 ? (\n <div className=\"flex items-center justify-center rounded-xl border border-border-50 bg-background p-6\">\n <Text size=\"sm\" className=\"text-text-600\">\n Este estudante ainda não respondeu nenhum simulado.\n </Text>\n </div>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {list.simulations.data.map((simulation, index) => (\n <SimulationItem\n // Keyed by the student too: the comment fields below keep\n // a dirty draft through a `value` change so an in-flight\n // save cannot discard it, and a note written for one\n // student must never survive into another. Remounting\n // resets it for free.\n key={`${student.userInstitutionId}-${simulation.id}`}\n simulation={simulation}\n index={index}\n expanded={expandedId === simulation.id}\n onToggle={() => toggle(simulation.id)}\n detail={details[simulation.id]}\n note={notes[simulation.id]}\n onRetryNote={() => retryNote(simulation.id)}\n onSaveNote={makeSaveNote(simulation.id)}\n onSaveQuestionComment={makeSaveQuestionComment(\n simulation.id\n )}\n />\n ))}\n </div>\n )}\n </section>\n )}\n </section>\n )}\n </Modal>\n );\n}\n","import { formatTimeSpent } from './activityDetailsUtils';\n\n/**\n * Format the time a student spent on a single question, the way the relatórios\n * of simulados present it: `HH:MM:SS` (`00:00:40`, `00:02:30`, `01:05:30`).\n *\n * The clock format is shared with `formatTimeSpent`, which renders the activity\n * and simulado totals — a question row and the \"Tempo total\" card above it read\n * the same way instead of each inventing its own notation.\n *\n * What this adds on top is the \"not measured\" rule: it returns `null` when\n * there is nothing meaningful to show — the value is zero, absent, negative or\n * not finite. Zero is not the same as \"answered instantly\": simulations\n * answered before per-question telemetry existed carry a zero, and so does a\n * question the student never opened. Rendering `00:00:00` there would read as\n * \"the student guessed\", which is the opposite of what the teacher should\n * conclude. Callers drop the whole segment from the row instead.\n *\n * @param seconds - Time spent in seconds\n * @returns Formatted duration in `HH:MM:SS`, or null when there is no measured time\n *\n * @example\n * ```typescript\n * formatQuestionDuration(150); // \"00:02:30\"\n * formatQuestionDuration(0); // null\n * ```\n */\nexport function formatQuestionDuration(\n seconds: number | null | undefined\n): string | null {\n if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {\n return null;\n }\n\n // Truncate before the zero check, not after: a fractional value below one\n // second floors to 0 and must take the same \"not measured\" path as a plain\n // 0, otherwise it would render a misleading `00:00:00`.\n const total = Math.floor(seconds);\n if (total <= 0) {\n return null;\n }\n\n return formatTimeSpent(total);\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { BaseApiClient } from '../types/api';\nimport type { NoteData, SimulationDetailData } from '../types/simulations';\nimport { createUseSimulations } from './useSimulations';\n\n/** Lazily loaded question detail of one simulado. */\nexport interface SimulationDetailState {\n readonly loading: boolean;\n readonly error: string | null;\n readonly data: SimulationDetailData | null;\n}\n\n/**\n * Lazily loaded teacher observation of one simulado.\n *\n * `error` exists so a failed request is never shown as \"this simulado has no\n * observation\": that would offer the teacher an empty field whose save is a\n * create-or-replace, silently overwriting a note they were never shown.\n */\nexport interface SimulationNoteState {\n readonly loading: boolean;\n readonly error: string | null;\n readonly data: NoteData | null;\n}\n\n/** Save handler of one simulado's observation, as the note row expects it. */\nexport type SaveSimulationNote = (\n text: string,\n file: File | null,\n existingAttachment: string | null\n) => Promise<void>;\n\n/**\n * What one simulado card needs from this hook: the two loaded pieces and the\n * three actions on them, already bound to that simulado.\n */\nexport interface SimulationCardHandlers {\n readonly detail: SimulationDetailState | undefined;\n readonly note: SimulationNoteState | undefined;\n /** Load the observation again after a failed request */\n readonly onRetryNote: () => void;\n readonly onSaveNote: SaveSimulationNote;\n readonly onSaveQuestionComment: (\n questionId: string,\n comment: string\n ) => Promise<void>;\n}\n\nexport interface UseSimulationCardDetailsParams {\n /** API client used to load and save the per-simulado data */\n readonly api: BaseApiClient;\n /** Enrollment whose simulados are listed; null while there is no student */\n readonly userInstitutionId: string | null;\n /** Whether the modal is open; reopening starts a fresh session */\n readonly isOpen: boolean;\n /**\n * Whether expanding a card loads its questions and observation.\n *\n * Those two come from `/performance/simulations/students/...`, which the\n * backend answers for simulados and activities but not for every activity\n * type. A modal listing a type it does not serve still expands its cards —\n * the stat and content cards travel with the list — but must not ask for a\n * detail that would 404. Default true.\n */\n readonly enabled?: boolean;\n}\n\nexport interface UseSimulationCardDetailsReturn {\n readonly expandedId: string | null;\n readonly details: Record<string, SimulationDetailState>;\n readonly notes: Record<string, SimulationNoteState>;\n /** Expand or collapse one simulado, loading its data the first time */\n readonly toggle: (simulationId: string) => void;\n /** Load the observation again after a failed request */\n readonly retryNote: (simulationId: string) => void;\n readonly makeSaveNote: (simulationId: string) => SaveSimulationNote;\n readonly makeSaveQuestionComment: (\n simulationId: string\n ) => (questionId: string, comment: string) => Promise<void>;\n}\n\n/**\n * Per-simulado detail state of a student's simulados modal.\n *\n * Owns what both modals that list simulados need: which card is expanded, the\n * questions and observation of each expanded card, and the two save actions.\n * The list itself stays with the caller, because each modal gets it from a\n * different endpoint.\n *\n * Responses are tagged with a session. The session changes whenever the modal\n * opens, the student changes or the API client changes, and a response from an\n * older session is dropped instead of written into the current one.\n *\n * @param params - API client, the enrollment being shown and the open flag\n * @returns Expanded card, loaded details and notes, and the actions on them\n *\n * @example\n * ```tsx\n * const { expandedId, details, notes, toggle } = useSimulationCardDetails({\n * api,\n * userInstitutionId: student?.userInstitutionId ?? null,\n * isOpen,\n * });\n * ```\n */\nexport function useSimulationCardDetails({\n api,\n userInstitutionId,\n isOpen,\n enabled = true,\n}: UseSimulationCardDetailsParams): UseSimulationCardDetailsReturn {\n const useSimulations = useMemo(() => createUseSimulations(api), [api]);\n const {\n fetchSimulationDetail,\n fetchNote,\n uploadNoteAttachment,\n saveNote,\n saveQuestionComment,\n } = useSimulations();\n\n const [expandedId, setExpandedId] = useState<string | null>(null);\n const [details, setDetails] = useState<Record<string, SimulationDetailState>>(\n {}\n );\n const [notes, setNotes] = useState<Record<string, SimulationNoteState>>({});\n\n // Guards state updates against the modal unmounting mid-request.\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // Bumped on every new session, so a slow response from the previous student\n // (or from a replaced API client) is dropped instead of written into the\n // current one.\n const sessionRef = useRef(0);\n useEffect(() => {\n sessionRef.current += 1;\n setExpandedId(null);\n setDetails({});\n setNotes({});\n }, [isOpen, userInstitutionId, api]);\n\n const isStale = useCallback(\n (session: number) => !mountedRef.current || sessionRef.current !== session,\n []\n );\n\n const loadDetail = useCallback(\n (enrollmentId: string, simulationId: string) => {\n const session = sessionRef.current;\n setDetails((previous) => ({\n ...previous,\n [simulationId]: { loading: true, error: null, data: null },\n }));\n fetchSimulationDetail(enrollmentId, simulationId)\n .then((data) => {\n if (isStale(session)) return;\n setDetails((previous) => ({\n ...previous,\n [simulationId]: { loading: false, error: null, data },\n }));\n })\n .catch(() => {\n if (isStale(session)) return;\n setDetails((previous) => ({\n ...previous,\n [simulationId]: {\n loading: false,\n error: 'Erro ao carregar as respostas',\n data: null,\n },\n }));\n });\n },\n [fetchSimulationDetail, isStale]\n );\n\n const loadNote = useCallback(\n (enrollmentId: string, simulationId: string) => {\n const session = sessionRef.current;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: { loading: true, error: null, data: null },\n }));\n fetchNote(enrollmentId, simulationId)\n .then((data) => {\n if (isStale(session)) return;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: { loading: false, error: null, data },\n }));\n })\n .catch(() => {\n if (isStale(session)) return;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: {\n loading: false,\n error: 'Erro ao carregar a observação',\n data: null,\n },\n }));\n });\n },\n [fetchNote, isStale]\n );\n\n const toggle = useCallback(\n (simulationId: string) => {\n if (!userInstitutionId) return;\n const next = expandedId === simulationId ? null : simulationId;\n setExpandedId(next);\n // Collapsing loads nothing; so does a list whose items have no detail\n // endpoint behind them (see `enabled`).\n if (!next || !enabled) return;\n\n // The two requests are guarded separately: a loaded detail must not stop\n // the observation from being fetched, and the other way around.\n if (!details[simulationId]) loadDetail(userInstitutionId, simulationId);\n if (!notes[simulationId]) loadNote(userInstitutionId, simulationId);\n },\n [\n userInstitutionId,\n enabled,\n expandedId,\n details,\n notes,\n loadDetail,\n loadNote,\n ]\n );\n\n // `enabled` can turn on under an already expanded card — a caller that\n // decides it from the list's kind may flip it without touching the card.\n // `toggle` ran while disabled and loaded nothing, so this catches the open\n // card up; the same guards keep it from repeating a request `toggle` made.\n useEffect(() => {\n if (!enabled || !expandedId || !userInstitutionId) return;\n if (!details[expandedId]) loadDetail(userInstitutionId, expandedId);\n if (!notes[expandedId]) loadNote(userInstitutionId, expandedId);\n }, [\n enabled,\n expandedId,\n userInstitutionId,\n details,\n notes,\n loadDetail,\n loadNote,\n ]);\n\n const retryNote = useCallback(\n (simulationId: string) => {\n if (!userInstitutionId) return;\n loadNote(userInstitutionId, simulationId);\n },\n [userInstitutionId, loadNote]\n );\n\n /**\n * Save the observation of one simulado, uploading a newly chosen file first\n * so only its public URL travels with the note.\n */\n const makeSaveNote = useCallback(\n (simulationId: string): SaveSimulationNote =>\n async (text, file, existingAttachment) => {\n if (!userInstitutionId) return;\n const session = sessionRef.current;\n const attachment = file\n ? await uploadNoteAttachment(file)\n : existingAttachment;\n const saved = await saveNote(\n userInstitutionId,\n simulationId,\n text,\n attachment\n );\n if (isStale(session)) return;\n setNotes((previous) => ({\n ...previous,\n [simulationId]: { loading: false, error: null, data: saved },\n }));\n },\n [userInstitutionId, uploadNoteAttachment, saveNote, isStale]\n );\n\n /**\n * Save a comment on one question and reflect it in the loaded detail, so the\n * field shows what the server now holds without a refetch.\n */\n const makeSaveQuestionComment = useCallback(\n (simulationId: string) => async (questionId: string, comment: string) => {\n if (!userInstitutionId) return;\n const session = sessionRef.current;\n const saved = await saveQuestionComment(\n userInstitutionId,\n simulationId,\n questionId,\n comment\n );\n if (isStale(session)) return;\n setDetails((previous) => {\n const current = previous[simulationId];\n if (!current?.data) return previous;\n return {\n ...previous,\n [simulationId]: {\n ...current,\n data: {\n ...current.data,\n questions: current.data.questions.map((question) =>\n question.questionId === questionId\n ? { ...question, teacherComment: saved?.teacherComment ?? '' }\n : question\n ),\n },\n },\n };\n });\n },\n [userInstitutionId, saveQuestionComment, isStale]\n );\n\n return {\n expandedId,\n details,\n notes,\n toggle,\n retryNote,\n makeSaveNote,\n makeSaveQuestionComment,\n };\n}\n","/**\n * Types of the student simulations details modal.\n *\n * They mirror `POST /performance/simulated/activities/student-simulations`,\n * the teacher/manager-scoped endpoint that answers everything the modal shows\n * for one student under a given simulado cut and time window.\n */\n\n/**\n * Performance band of a student, as classified by the server.\n *\n * With `performanceMode=absolute` the server derives the band from the\n * percentage: >=90 HIGHLIGHT, >=70 ABOVE_AVERAGE, >=40 BELOW_AVERAGE, else\n * ATTENTION_POINT. See {@link SIMULATED_SIMULATIONS_TAG_CONFIG}.\n */\nexport type SimulatedPerformanceTag =\n | 'HIGHLIGHT'\n | 'ABOVE_AVERAGE'\n | 'BELOW_AVERAGE'\n | 'ATTENTION_POINT';\n\n/**\n * Label and badge colours per band.\n *\n * The colours are the gestor's map-token palette for the same bands, so the\n * ranking tables, the distribution pie and this badge all read alike.\n *\n * Deliberately separate from `SIMULATED_PERFORMANCE_TAG_CONFIG` in\n * `SimulatedStudentDetailsModal`: that one maps a band to a `variant` for the\n * ENEM stack's own badge, while this one carries explicit classes. Merging them\n * would repaint the ENEM screens.\n */\nexport const SIMULATED_SIMULATIONS_TAG_CONFIG: Record<\n SimulatedPerformanceTag,\n { label: string; badgeClassName: string }\n> = {\n HIGHLIGHT: {\n label: 'Destaque da turma',\n badgeClassName: 'bg-success-300 text-text',\n },\n ABOVE_AVERAGE: {\n label: 'Acima da média',\n badgeClassName: 'bg-indicator-positive text-warning-900',\n },\n BELOW_AVERAGE: {\n label: 'Abaixo da média',\n badgeClassName: 'bg-warning-400 text-text',\n },\n ATTENTION_POINT: {\n label: 'Ponto de atenção',\n badgeClassName: 'bg-error-700 text-text-100',\n },\n};\n\n/** A content (subtema) with the student's hit rate on it. */\nexport interface StudentContentHitRate {\n contentId: string;\n contentName: string;\n correct: number;\n totalQuestions: number;\n /** 0-100 */\n correctPercentage: number;\n}\n\n/**\n * One simulado the student answered, as the details modal lists it.\n *\n * `score` is a 0-100 percentage shown as a 0-10 grade; `answeredAt` is an ISO\n * string, null when the server has no date for the attempt.\n */\nexport interface StudentSimulationItem {\n activityId: string;\n title: string;\n subtype: string | null;\n score: number;\n timeSpentSeconds: number;\n answeredAt: string | null;\n correct: number;\n incorrect: number;\n blank: number;\n totalQuestions: number;\n bestContent: StudentContentHitRate | null;\n worstContent: StudentContentHitRate | null;\n}\n\n/** Everything the student details modal shows, for one student and one cut. */\nexport interface StudentSimulationsData {\n student: {\n studentId: string;\n userInstitutionId: string;\n institutionId: string | null;\n name: string;\n school: string | null;\n schoolYear: string | null;\n class: string | null;\n /** 0-100, averaged over the listed simulados */\n average: number;\n performance: SimulatedPerformanceTag;\n };\n totals: {\n simulationsCount: number;\n totalTimeSeconds: number;\n correct: number;\n incorrect: number;\n blank: number;\n };\n bestContent: StudentContentHitRate | null;\n worstContent: StudentContentHitRate | null;\n simulations: StudentSimulationItem[];\n /**\n * Activities assigned in the period the student has not answered yet.\n * Listed after the answered ones; absent (or empty) lists nothing extra.\n */\n pending?: StudentPendingActivity[];\n /**\n * The student's logins in the period. Rendered as its own section when\n * present; the Simulados report leaves it out.\n */\n access?: StudentAccessSummary;\n}\n\n/** One activity assigned to the student and still unanswered. */\nexport interface StudentPendingActivity {\n activityId: string;\n title: string;\n subtype: string | null;\n}\n\n/** How present the student was in the period. */\nexport interface StudentAccessSummary {\n accessCount: number;\n /** Minutes across activities and lessons, as the access report counts. */\n totalTimeMinutes: number;\n /** ISO string of the last login, null when the student never logged in. */\n lastAccess: string | null;\n}\n","import type { ReactNode } from 'react';\nimport Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport { SkeletonCard } from '../Skeleton/Skeleton';\nimport { SimulationAnswers } from '../SimulationsPage/SimulationsDetailModal';\nimport {\n buildSimulationMeta,\n ContentCards,\n DataCard,\n SectionTitle,\n SimulationCardShell,\n SimulationStatCards,\n StudentSummaryHeader,\n} from '../shared/SimulationSummaryCards';\nimport {\n useSimulationCardDetails,\n type SimulationCardHandlers,\n} from '../../hooks/useSimulationCardDetails';\nimport type { BaseApiClient } from '../../types/api';\nimport { formatTimeSpent } from '../../utils/activityDetailsUtils';\nimport dayjs from 'dayjs';\nimport {\n SIMULATED_SIMULATIONS_TAG_CONFIG,\n type SimulatedPerformanceTag,\n type StudentAccessSummary,\n type StudentPendingActivity,\n type StudentSimulationItem,\n type StudentSimulationsData,\n} from './types';\n\n/**\n * Band badge, in the gestor tables' palette.\n *\n * Exported so a consuming table can label its Desempenho column with exactly\n * the badge the modal header shows.\n */\nexport function PerformanceBadge({\n tag,\n}: {\n readonly tag: SimulatedPerformanceTag;\n}) {\n const config = SIMULATED_SIMULATIONS_TAG_CONFIG[tag];\n if (!config) return null;\n\n return (\n <Badge\n variant=\"solid\"\n action=\"neutral\"\n size=\"small\"\n className={config.badgeClassName}\n >\n {config.label}\n </Badge>\n );\n}\n\n/**\n * The copy that names what the list holds.\n *\n * The modal was written for simulados and its defaults say so; the teacher's\n * Atividades report lists activities through the same endpoint and hands over\n * its own wording. Only these four strings differ between the two — the\n * \"Tempo total\" and \"Desempenho geral\" blocks read the same either way.\n */\nexport interface SimulatedStudentSimulationsModalLabels {\n /** Title of the first section, e.g. \"Dados de simulados\" */\n readonly dataSection: string;\n /** Label of the count card, e.g. \"Simulados realizados\" */\n readonly completedCount: string;\n /**\n * Label of a third card of the data section, with the questions the\n * student answered (correct + incorrect + blank). Left out, the card is\n * not rendered — the Simulados report does not show it.\n */\n readonly questionsAnswered?: string;\n /** Title of the list section, e.g. \"Simulados realizados\" */\n readonly listTitle: string;\n /** Shown when the list is empty, e.g. \"Nenhum simulado no período\" */\n readonly empty: string;\n /** Title of the logins section, rendered when `data.access` is present */\n readonly accessSection: string;\n readonly accessCount: string;\n readonly timeOnline: string;\n readonly lastLogin: string;\n /** Shown inside the card of an assigned activity not answered yet */\n readonly pendingMessage: string;\n /** Placeholder of the observation editor of an expanded card */\n readonly notePlaceholder: string;\n}\n\n/** The simulado wording, applied when the caller names nothing. */\nexport const DEFAULT_SIMULATED_STUDENT_SIMULATIONS_LABELS: SimulatedStudentSimulationsModalLabels =\n {\n dataSection: 'Dados de simulados',\n completedCount: 'Simulados realizados',\n listTitle: 'Simulados realizados',\n empty: 'Nenhum simulado no período',\n accessSection: 'Dados de acesso',\n accessCount: 'Quantidade de acessos',\n timeOnline: 'Tempo total online',\n lastLogin: 'Último login',\n pendingMessage: 'Sem dados ainda! A atividade ainda não foi feita.',\n notePlaceholder: 'Escreva uma observação para este simulado',\n };\n\n/** A count as the design writes it: at least two digits, so 6 reads \"06\". */\nfunction formatCount(value: number): string {\n return String(value).padStart(2, '0');\n}\n\n/**\n * \"dd/mm/aaaa • HH:mmh\" of the last login. A student who never logged in\n * gets the zeroed date the design uses as its placeholder, not a dash.\n */\nfunction formatLastAccess(lastAccess: string | null): string {\n return lastAccess\n ? dayjs(lastAccess).format('DD/MM/YYYY • HH:mm[h]')\n : '00/00/0000 • 00:00h';\n}\n\n/** What the list shows when there is nothing to list, answered or pending. */\nexport interface SimulatedStudentSimulationsEmptyState {\n /** Illustration above the title; omitted, only the text is shown */\n readonly image?: string;\n readonly title: string;\n readonly description: string;\n}\n\n/**\n * Empty list with an illustration: the teacher has not assigned anything to\n * this student yet, so there is nothing to answer, let alone to show.\n */\nfunction ListEmptyState({\n image,\n title,\n description,\n}: SimulatedStudentSimulationsEmptyState) {\n return (\n <div className=\"flex min-h-[250px] flex-col items-center justify-center gap-4 rounded-xl border border-border-50 bg-background p-6\">\n {image && <img src={image} alt=\"\" className=\"h-20 w-auto\" />}\n <div className=\"flex flex-col items-center gap-1\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-center text-text-950\">\n {title}\n </Text>\n <Text size=\"xs\" className=\"text-center text-text-500\">\n {description}\n </Text>\n </div>\n </div>\n );\n}\n\n/** The logins section: how often, for how long and when last. */\nfunction AccessSection({\n access,\n labels,\n}: {\n readonly access: StudentAccessSummary;\n readonly labels: SimulatedStudentSimulationsModalLabels;\n}) {\n return (\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>{labels.accessSection}</SectionTitle>\n <div className=\"flex flex-col gap-2 md:flex-row\">\n <DataCard\n label={labels.accessCount}\n value={formatCount(access.accessCount)}\n />\n <DataCard\n label={labels.timeOnline}\n value={formatTimeSpent(access.totalTimeMinutes * 60)}\n />\n <DataCard\n label={labels.lastLogin}\n value={formatLastAccess(access.lastAccess)}\n />\n </div>\n </section>\n );\n}\n\n/**\n * One assigned activity the student has not answered yet.\n *\n * Same frame as the answered cards, but nothing to expand: there is no score,\n * no answers and no date, only the title and the reason the card is empty.\n */\nfunction PendingCard({\n activity,\n message,\n}: {\n readonly activity: StudentPendingActivity;\n readonly message: string;\n}) {\n return (\n <div className=\"flex flex-col gap-2 rounded-xl border border-border-200 bg-background p-4\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n {activity.title}\n </Text>\n <Text size=\"xs\" className=\"text-text-500\">\n {message}\n </Text>\n </div>\n );\n}\n\n/** One expandable simulado of the \"Simulados realizados\" list. */\nfunction SimulationCard({\n simulation,\n expanded,\n showAnswers,\n notePlaceholder,\n onToggle,\n detail,\n note,\n onRetryNote,\n onSaveNote,\n onSaveQuestionComment,\n}: SimulationCardHandlers & {\n readonly simulation: StudentSimulationItem;\n readonly expanded: boolean;\n /** Whether the expanded card lists the questions and the observation */\n readonly showAnswers: boolean;\n /** Placeholder of the observation editor */\n readonly notePlaceholder: string;\n readonly onToggle: () => void;\n}) {\n return (\n <SimulationCardShell\n value={simulation.activityId}\n title={simulation.title}\n meta={buildSimulationMeta(simulation)}\n correct={simulation.correct}\n totalQuestions={simulation.totalQuestions}\n expanded={expanded}\n onToggle={onToggle}\n >\n <SimulationStatCards\n score={simulation.score}\n correct={simulation.correct}\n incorrect={simulation.incorrect}\n blank={simulation.blank}\n />\n <ContentCards\n best={simulation.bestContent}\n worst={simulation.worstContent}\n />\n {expanded && showAnswers && (\n <SimulationAnswers\n detail={detail}\n note={note}\n onRetryNote={onRetryNote}\n notePlaceholder={notePlaceholder}\n onSaveNote={onSaveNote}\n onSaveQuestionComment={onSaveQuestionComment}\n />\n )}\n </SimulationCardShell>\n );\n}\n\n/** Loading placeholder of the whole modal. */\nfunction ModalSkeleton() {\n return (\n <div className=\"flex flex-col gap-4\">\n <SkeletonCard className=\"min-h-[72px]\" />\n <SkeletonCard className=\"min-h-[120px]\" />\n <SkeletonCard className=\"min-h-[200px]\" />\n </div>\n );\n}\n\n/**\n * Student details modal of the Simulados report, opened by clicking a row of\n * \"Desempenho por estudante\".\n *\n * The summary data, loading and error are owned by the caller: that request\n * depends on the tab and the time window the report is on, and the report\n * already knows both. The `title` arrives ready for the same reason — the\n * professor app names a fixed period while the gestor may name an arbitrary\n * calendar range. What this modal owns is the per-simulado detail (observation\n * and questions), fetched from the Simulados endpoints when a card is first\n * expanded.\n */\nexport interface SimulatedStudentSimulationsModalProps {\n /** API client used to load each simulado's questions and observation */\n readonly api: BaseApiClient;\n readonly isOpen: boolean;\n readonly onClose: () => void;\n /** Modal title, e.g. \"Simulados Enem em 1 mês\" */\n readonly title: string;\n readonly data: StudentSimulationsData | null;\n readonly loading: boolean;\n readonly error: string | null;\n /** Wording of the list; whatever is omitted keeps the simulado copy */\n readonly labels?: Partial<SimulatedStudentSimulationsModalLabels>;\n /**\n * Whether an expanded card loads and lists its questions and observation.\n * Off for lists of a type the detail endpoint does not serve (it answers\n * for simulados and activities), so the card shows its stat and content\n * cards alone. Default true.\n */\n readonly detailsEnabled?: boolean;\n /** Where the band badge sits in the header. Default `start`. */\n readonly badgePlacement?: 'start' | 'end';\n /**\n * Whether to render the logins section from `data.access`. Off by default:\n * the endpoint answers it for every caller, but only the Atividades\n * ranking modal shows it. Default false.\n */\n readonly showAccess?: boolean;\n /**\n * Whether to list the assignments of `data.pending` after the answered\n * ones. Off by default, for the same reason. Default false.\n */\n readonly showPending?: boolean;\n /**\n * Illustrated empty state of the list, shown when the student has nothing\n * answered and nothing pending. Omitted, the list shows `labels.empty`\n * as a single line.\n */\n readonly emptyState?: SimulatedStudentSimulationsEmptyState;\n}\n\nexport function SimulatedStudentSimulationsModal({\n api,\n isOpen,\n onClose,\n title,\n data,\n loading,\n error,\n labels,\n detailsEnabled = true,\n badgePlacement = 'start',\n showAccess = false,\n showPending = false,\n emptyState,\n}: SimulatedStudentSimulationsModalProps) {\n const copy = { ...DEFAULT_SIMULATED_STUDENT_SIMULATIONS_LABELS, ...labels };\n const {\n expandedId,\n details,\n notes,\n toggle,\n retryNote,\n makeSaveNote,\n makeSaveQuestionComment,\n } = useSimulationCardDetails({\n api,\n userInstitutionId: data?.student.userInstitutionId ?? null,\n isOpen,\n enabled: detailsEnabled,\n });\n\n let content: ReactNode = null;\n if (loading) {\n content = <ModalSkeleton />;\n } else if (error) {\n content = (\n // `role=\"alert\"`: a falha substitui o conteúdo e precisa ser anunciada.\n <div\n className=\"flex min-h-[160px] items-center justify-center\"\n role=\"alert\"\n >\n <Text size=\"sm\" className=\"text-text-500\">\n {error}\n </Text>\n </div>\n );\n } else if (data) {\n const pending = showPending ? (data.pending ?? []) : [];\n // Nothing answered in the period: the totals are all zero and the\n // average is meaningless, so the cards show placeholders, not results.\n const hasAnswers = data.simulations.length > 0;\n const listIsEmpty = !hasAnswers && pending.length === 0;\n content = (\n <div className=\"flex flex-col gap-6\">\n <StudentSummaryHeader\n name={data.student.name}\n location={[\n data.student.school,\n data.student.class,\n data.student.schoolYear,\n ]}\n badge={<PerformanceBadge tag={data.student.performance} />}\n badgePlacement={badgePlacement}\n />\n\n {showAccess && data.access && (\n <AccessSection access={data.access} labels={copy} />\n )}\n\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>{copy.dataSection}</SectionTitle>\n <div className=\"flex flex-col gap-2 md:flex-row\">\n <DataCard\n label={copy.completedCount}\n value={\n hasAnswers\n ? String(data.totals.simulationsCount)\n : formatCount(0)\n }\n />\n {copy.questionsAnswered && (\n <DataCard\n label={copy.questionsAnswered}\n value={String(\n data.totals.correct +\n data.totals.incorrect +\n data.totals.blank\n )}\n />\n )}\n <DataCard\n label=\"Tempo total\"\n value={formatTimeSpent(data.totals.totalTimeSeconds)}\n />\n </div>\n </section>\n\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>Desempenho geral</SectionTitle>\n <SimulationStatCards\n score={data.student.average}\n correct={data.totals.correct}\n incorrect={data.totals.incorrect}\n blank={data.totals.blank}\n empty={!hasAnswers}\n />\n <ContentCards best={data.bestContent} worst={data.worstContent} />\n </section>\n\n <section className=\"flex flex-col gap-3\">\n <SectionTitle>{copy.listTitle}</SectionTitle>\n {listIsEmpty && emptyState && <ListEmptyState {...emptyState} />}\n {listIsEmpty && !emptyState && (\n <div className=\"flex items-center justify-center rounded-xl border border-border-50 bg-background p-6\">\n <Text size=\"sm\" className=\"text-text-500\">\n {copy.empty}\n </Text>\n </div>\n )}\n {!listIsEmpty && (\n <div className=\"flex flex-col gap-2\">\n {data.simulations.map((simulation) => (\n <SimulationCard\n // Keyed by the student too: the comment fields keep a dirty\n // draft through a `value` change, and a draft written for\n // one student must never survive into another.\n key={`${data.student.userInstitutionId}-${simulation.activityId}`}\n simulation={simulation}\n expanded={expandedId === simulation.activityId}\n showAnswers={detailsEnabled}\n notePlaceholder={copy.notePlaceholder}\n onToggle={() => toggle(simulation.activityId)}\n detail={details[simulation.activityId]}\n note={notes[simulation.activityId]}\n onRetryNote={() => retryNote(simulation.activityId)}\n onSaveNote={makeSaveNote(simulation.activityId)}\n onSaveQuestionComment={makeSaveQuestionComment(\n simulation.activityId\n )}\n />\n ))}\n {pending.map((activity) => (\n <PendingCard\n key={`${data.student.userInstitutionId}-pending-${activity.activityId}`}\n activity={activity}\n message={copy.pendingMessage}\n />\n ))}\n </div>\n )}\n </section>\n </div>\n );\n }\n\n return (\n <Modal isOpen={isOpen} onClose={onClose} title={title} size=\"xl\">\n {content}\n </Modal>\n );\n}\n"]}
|