UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1 lines 127 kB
{"version":3,"sources":["../src/components/SimulatedPerformance/useSimulatedPerformance.tsx","../src/components/GeneralOverviewSection/GeneralOverviewSection.tsx","../src/components/GeneralOverviewSection/useGeneralOverview.ts","../src/components/SimulatedStudentsOverview/useSimulatedOverview.ts","../src/components/SimulatedStudentDetailsModal/types.ts","../src/components/SimulatedStudentsOverview/utils.ts","../src/components/SimulatedStudentsOverview/useAggregatedOverview.ts","../src/components/SimulatedContentsPerformance/useSimulatedContents.ts","../src/components/AreaKnowledgeSelector/AreaKnowledgeSelector.tsx","../src/components/AreaKnowledgeSelector/types.ts","../src/components/SimulatedStudentDetailsModal/SimulatedStudentDetailsModal.tsx","../src/components/SimulatedStudentDetailsModal/useSimulatedStudentDetails.ts","../src/components/SimulatedStudentDetailsModal/utils.ts","../src/utils/profileAggregation.ts","../src/components/SimulatedPerformance/types.ts"],"sourcesContent":["import { useEffect, useCallback, useState, useMemo, useRef } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport Badge from '../Badge/Badge';\nimport { useGeneralOverview } from '../GeneralOverviewSection';\nimport {\n useSimulatedOverview,\n useAggregatedOverview,\n} from '../SimulatedStudentsOverview';\nimport { useSimulatedContents } from '../SimulatedContentsPerformance';\nimport { ESSAY_AREA_ID } from '../AreaKnowledgeSelector';\nimport { SIMULATED_PERFORMANCE_TAG_CONFIG } from '../SimulatedStudentDetailsModal';\nimport { formatScore } from '../../utils/utils';\nimport { getAggregationTypeByProfile } from '../../utils/profileAggregation';\nimport type { ColumnConfig } from '../TableProvider';\nimport type { StudentsHighlightPeriod } from '../../hooks/useStudentsHighlight';\nimport { ScoreType } from '../../types/common';\nimport type { SimulatedFilters } from '../SimulatedFilters/types';\nimport type { SimulatedStudentItem } from '../SimulatedStudentsOverview/types';\nimport type { SimulatedContentItem } from '../SimulatedContentsPerformance/types';\nimport {\n SimulatedViewTab,\n type UseSimulatedPerformanceOptions,\n type UseSimulatedPerformanceReturn,\n} from './types';\n\n/**\n * Performance tag cell renderer for simulated exams\n */\nfunction SimulatedPerformanceTagCell({\n row,\n}: {\n readonly row: SimulatedStudentItem;\n}) {\n const tagConfig = SIMULATED_PERFORMANCE_TAG_CONFIG[row.performance];\n\n return (\n <Badge variant=\"solid\" action={tagConfig.variant} size=\"small\">\n {tagConfig.label}\n </Badge>\n );\n}\n\n/**\n * Performance cell renderer for contents/skills with progress bar\n */\nfunction ContentPerformanceCell({\n percentage,\n}: {\n readonly percentage: number;\n}) {\n const getColorClass = (): string => {\n if (percentage >= 70) {\n return 'bg-success-500';\n }\n\n if (percentage >= 50) {\n return 'bg-warning-500';\n }\n\n return 'bg-error-500';\n };\n\n const colorClass = getColorClass();\n\n return (\n <div className=\"flex items-center justify-center gap-2\">\n <span className=\"text-sm text-text-950 min-w-[40px]\">\n {Math.round(percentage)}%\n </span>\n <div className=\"w-16 h-2 bg-background-100 rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${colorClass}`}\n style={{ width: `${percentage}%` }}\n />\n </div>\n </div>\n );\n}\n\n/**\n * Shared defaults for centered columns\n */\nconst centeredColumn: Partial<ColumnConfig<SimulatedStudentItem>> = {\n sortable: true,\n className: 'py-3 px-4 text-center',\n align: 'center',\n};\n\n/**\n * Shared defaults for centered columns (contents table)\n */\nconst centeredContentsColumn: Partial<ColumnConfig<SimulatedContentItem>> = {\n sortable: true,\n className: 'py-3 px-4 text-center',\n align: 'center',\n};\n\n/**\n * Base columns for simulated exams view (without render functions that depend on scoreType)\n */\nconst simulatedTableColumnsBase: Omit<\n ColumnConfig<SimulatedStudentItem>,\n 'render'\n>[] = [\n {\n key: 'name',\n label: 'Nome',\n sortable: true,\n className: 'py-3 px-4 text-start',\n },\n {\n key: 'school',\n label: 'Escola',\n sortable: false,\n className: 'py-3 px-4 text-start',\n },\n {\n ...centeredColumn,\n key: 'schoolYear',\n label: 'Ano',\n width: '80px',\n sortable: false,\n },\n {\n ...centeredColumn,\n key: 'class',\n label: 'Turma',\n width: '80px',\n sortable: false,\n },\n {\n ...centeredColumn,\n key: 'average',\n label: 'Média',\n width: '100px',\n },\n {\n ...centeredColumn,\n key: 'performance',\n label: 'Desempenho',\n width: '160px',\n },\n];\n\n/**\n * Table columns for simulated contents/skills view\n */\nconst contentsTableColumns: ColumnConfig<SimulatedContentItem>[] = [\n {\n key: 'contentName',\n label: 'Habilidade',\n sortable: true,\n className: 'py-3 px-4 text-start',\n },\n {\n key: 'subject',\n label: 'Componente',\n sortable: false,\n className: 'py-3 px-4 text-start',\n render: (_value: unknown, row: SimulatedContentItem) => row.subject.name,\n },\n {\n ...centeredContentsColumn,\n key: 'simulatedExamsCount',\n label: 'Simulados',\n width: '100px',\n },\n {\n ...centeredContentsColumn,\n key: 'questionsCount',\n label: 'Questões',\n width: '100px',\n },\n {\n ...centeredContentsColumn,\n key: 'studentsCount',\n label: 'Estudantes',\n width: '100px',\n },\n {\n ...centeredContentsColumn,\n key: 'performance',\n label: 'Desempenho',\n width: '160px',\n render: (_value: unknown, row: SimulatedContentItem) => (\n <ContentPerformanceCell percentage={row.performance.correctPercentage} />\n ),\n },\n];\n\n/**\n * Hook de orquestração para a funcionalidade de Simulados.\n * Gerencia estados, query params, carregamento de dados e handlers.\n */\nexport function useSimulatedPerformance({\n api,\n profileName,\n enabled = true,\n}: UseSimulatedPerformanceOptions): UseSimulatedPerformanceReturn {\n const [searchParams, setSearchParams] = useSearchParams();\n\n // === Aggregation Type based on Profile ===\n const aggregationType = useMemo(\n () => getAggregationTypeByProfile(profileName),\n [profileName]\n );\n\n // === Query Params ===\n const period = useMemo((): StudentsHighlightPeriod => {\n const periodParam = searchParams.get('period');\n if (periodParam === '7_DAYS') return '7_DAYS';\n if (periodParam === '1_MONTH') return '1_MONTH';\n if (periodParam === '3_MONTHS') return '3_MONTHS';\n if (periodParam === '6_MONTHS') return '6_MONTHS';\n if (periodParam === '1_YEAR') return '1_YEAR';\n return '1_MONTH';\n }, [searchParams]);\n\n const scoreType = useMemo((): ScoreType => {\n const scoreTypeParam = searchParams.get('scoreType');\n if (scoreTypeParam === 'tri') return ScoreType.TRI;\n return ScoreType.PERCENTAGE;\n }, [searchParams]);\n\n // === Estados de Seleção ===\n const [selectedAreaKnowledgeId, setSelectedAreaKnowledgeId] = useState<\n string | null\n >(null);\n const [selectedAreaRelatedIds, setSelectedAreaRelatedIds] = useState<\n string[]\n >([]);\n const [selectedSubjectId, setSelectedSubjectId] = useState<string | null>(\n null\n );\n const [simulatedViewTab, setSimulatedViewTab] = useState<SimulatedViewTab>(\n SimulatedViewTab.STUDENTS\n );\n\n // === Estados de Modais ===\n const [isStudentModalOpen, setIsStudentModalOpen] = useState(false);\n const [selectedStudent, setSelectedStudent] = useState<{\n userInstitutionId: string;\n name: string;\n } | null>(null);\n\n const [isContentModalOpen, setIsContentModalOpen] = useState(false);\n const [selectedContent, setSelectedContent] = useState<{\n contentId: string;\n contentName: string;\n } | null>(null);\n\n const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);\n\n // === Filtros ===\n const [filters, setFilters] = useState<SimulatedFilters>({\n schoolIds: [],\n schoolYearIds: [],\n classIds: [],\n studentsIds: [],\n });\n\n // === Refs para evitar closure issues ===\n const selectedAreaKnowledgeIdRef = useRef<string | null>(\n selectedAreaKnowledgeId\n );\n const selectedSubjectIdRef = useRef<string | null>(selectedSubjectId);\n const filtersRef = useRef<SimulatedFilters>(filters);\n const simulatedViewTabRef = useRef<SimulatedViewTab>(simulatedViewTab);\n const scoreTypeRef = useRef<ScoreType>(scoreType);\n const periodRef = useRef<StudentsHighlightPeriod>(period);\n const aggregationTypeRef = useRef(aggregationType);\n\n // Keep refs in sync\n useEffect(() => {\n selectedAreaKnowledgeIdRef.current = selectedAreaKnowledgeId;\n }, [selectedAreaKnowledgeId]);\n\n useEffect(() => {\n selectedSubjectIdRef.current = selectedSubjectId;\n }, [selectedSubjectId]);\n\n useEffect(() => {\n filtersRef.current = filters;\n }, [filters]);\n\n useEffect(() => {\n simulatedViewTabRef.current = simulatedViewTab;\n }, [simulatedViewTab]);\n\n useEffect(() => {\n scoreTypeRef.current = scoreType;\n }, [scoreType]);\n\n useEffect(() => {\n periodRef.current = period;\n }, [period]);\n\n useEffect(() => {\n aggregationTypeRef.current = aggregationType;\n }, [aggregationType]);\n\n // === Hooks de API ===\n const {\n data: generalOverviewData,\n loading: generalOverviewLoading,\n error: generalOverviewError,\n fetchOverview: fetchGeneralOverview,\n } = useGeneralOverview(api);\n\n // Aggregated overview based on profile (classes/municipalities/students)\n const {\n data: aggregatedData,\n loading: aggregatedLoading,\n isRefreshing: aggregatedRefreshing,\n error: aggregatedError,\n fetchOverview: fetchAggregatedOverview,\n } = useAggregatedOverview(api);\n\n // Paginated students overview for table\n const {\n data: simulatedData,\n loading: simulatedLoading,\n isRefreshing: simulatedRefreshing,\n error: simulatedError,\n fetchOverview: fetchSimulatedOverview,\n } = useSimulatedOverview(api);\n\n const {\n data: contentsData,\n loading: contentsLoading,\n isRefreshing: contentsRefreshing,\n error: contentsError,\n fetchContents,\n } = useSimulatedContents(api);\n\n // === Valores derivados ===\n const isEssaySelected = selectedAreaKnowledgeId === ESSAY_AREA_ID;\n\n const activeFiltersCount = useMemo(() => {\n let count = 0;\n if (filters.schoolIds.length > 0) count++;\n if (filters.schoolYearIds.length > 0) count++;\n if (filters.classIds.length > 0) count++;\n if (filters.studentsIds.length > 0) count++;\n return count;\n }, [filters]);\n\n // Sync selectedAreaRelatedIds when generalOverviewData is refreshed\n useEffect(() => {\n if (!selectedAreaKnowledgeId || !generalOverviewData?.areas) {\n return;\n }\n const area = generalOverviewData.areas.find(\n (a) => a.id === selectedAreaKnowledgeId\n );\n const relatedIds = area?.relatedIds ?? [selectedAreaKnowledgeId];\n setSelectedAreaRelatedIds(relatedIds);\n }, [generalOverviewData, selectedAreaKnowledgeId]);\n\n // === Colunas das Tabelas (memoizadas) ===\n const studentsTableColumns = useMemo(\n (): ColumnConfig<SimulatedStudentItem>[] =>\n simulatedTableColumnsBase.map((col) => {\n if (col.key === 'average') {\n return {\n ...col,\n render: (_value: unknown, row: SimulatedStudentItem) =>\n formatScore(row.average, scoreType),\n };\n }\n if (col.key === 'performance') {\n return {\n ...col,\n render: (_value: unknown, row: SimulatedStudentItem) => (\n <SimulatedPerformanceTagCell row={row} />\n ),\n };\n }\n return col as ColumnConfig<SimulatedStudentItem>;\n }),\n [scoreType]\n );\n\n // === Funções de Carregamento ===\n const loadStudentsData = useCallback(\n async (\n areaKnowledgeId: string | null,\n subjectId: string | null,\n page = 1,\n refresh = false,\n periodOverride?: StudentsHighlightPeriod,\n scoreTypeOverride?: ScoreType\n ) => {\n const currentFilters = filtersRef.current;\n const currentPeriod = periodOverride ?? periodRef.current;\n const currentScoreType = scoreTypeOverride ?? scoreTypeRef.current;\n const isEssay = areaKnowledgeId === ESSAY_AREA_ID;\n\n const effectiveSubjectId =\n !isEssay && subjectId && subjectId !== 'all' ? subjectId : undefined;\n const effectiveAreaKnowledgeId =\n !isEssay && areaKnowledgeId && areaKnowledgeId !== 'all'\n ? areaKnowledgeId\n : undefined;\n\n await fetchSimulatedOverview(\n {\n simulationType: isEssay ? 'essays' : 'enem-1',\n period: currentPeriod,\n subjectId: effectiveSubjectId,\n areaKnowledgeId: effectiveAreaKnowledgeId,\n schoolIds: currentFilters.schoolIds,\n schoolYearIds: currentFilters.schoolYearIds,\n classIds: currentFilters.classIds,\n studentsIds: currentFilters.studentsIds,\n page,\n limit: 10,\n orderBy: 'name',\n order: 'asc',\n scoreType: currentScoreType,\n },\n refresh\n );\n },\n [fetchSimulatedOverview]\n );\n\n const loadSkillsData = useCallback(\n async (\n areaKnowledgeId: string | null,\n subjectId: string | null,\n periodOverride?: StudentsHighlightPeriod,\n scoreTypeOverride?: ScoreType\n ) => {\n // Essays não têm conteúdos/habilidades\n if (areaKnowledgeId === ESSAY_AREA_ID) {\n return;\n }\n\n const currentFilters = filtersRef.current;\n const currentPeriod = periodOverride ?? periodRef.current;\n const currentScoreType = scoreTypeOverride ?? scoreTypeRef.current;\n\n const effectiveSubjectId =\n subjectId && subjectId !== 'all' ? subjectId : undefined;\n const effectiveAreaKnowledgeId =\n areaKnowledgeId && areaKnowledgeId !== 'all'\n ? areaKnowledgeId\n : undefined;\n\n await fetchContents({\n simulationType: 'enem-1',\n period: currentPeriod,\n subjectId: effectiveSubjectId,\n areaKnowledgeId: effectiveAreaKnowledgeId,\n schoolIds: currentFilters.schoolIds,\n schoolYearIds: currentFilters.schoolYearIds,\n classIds: currentFilters.classIds,\n studentsIds: currentFilters.studentsIds,\n page: 1,\n limit: 10,\n orderBy: 'correctPercentage',\n order: 'asc',\n scoreType: currentScoreType,\n });\n },\n [fetchContents]\n );\n\n const loadGeneralOverviewData = useCallback(\n async (\n periodOverride?: StudentsHighlightPeriod,\n scoreTypeOverride?: ScoreType\n ) => {\n const currentFilters = filtersRef.current;\n const currentScoreType = scoreTypeOverride ?? scoreTypeRef.current;\n\n await fetchGeneralOverview({\n period: periodOverride ?? periodRef.current,\n schoolIds: currentFilters.schoolIds,\n schoolYearIds: currentFilters.schoolYearIds,\n classIds: currentFilters.classIds,\n studentsIds: currentFilters.studentsIds,\n scoreType: currentScoreType,\n });\n },\n [fetchGeneralOverview]\n );\n\n /**\n * Load aggregated overview data based on profile (classes/municipalities/students)\n */\n const loadAggregatedOverviewData = useCallback(\n async (\n areaKnowledgeId: string | null,\n subjectId: string | null,\n refresh = false,\n periodOverride?: StudentsHighlightPeriod,\n scoreTypeOverride?: ScoreType\n ) => {\n const currentFilters = filtersRef.current;\n const currentPeriod = periodOverride ?? periodRef.current;\n const currentScoreType = scoreTypeOverride ?? scoreTypeRef.current;\n const currentAggregationType = aggregationTypeRef.current;\n const isEssay = areaKnowledgeId === ESSAY_AREA_ID;\n\n const effectiveSubjectId =\n !isEssay && subjectId && subjectId !== 'all' ? subjectId : undefined;\n const effectiveAreaKnowledgeId =\n !isEssay && areaKnowledgeId && areaKnowledgeId !== 'all'\n ? areaKnowledgeId\n : undefined;\n\n await fetchAggregatedOverview(\n {\n aggregationType: currentAggregationType,\n simulationType: isEssay ? 'essays' : 'enem-1',\n period: currentPeriod,\n subjectId: effectiveSubjectId,\n areaKnowledgeId: effectiveAreaKnowledgeId,\n schoolIds: currentFilters.schoolIds,\n schoolYearIds: currentFilters.schoolYearIds,\n classIds: currentFilters.classIds,\n studentsIds: currentFilters.studentsIds,\n scoreType: currentScoreType,\n },\n refresh\n );\n },\n [fetchAggregatedOverview]\n );\n\n // === Handlers ===\n const handlePeriodChange = useCallback(\n (value: string) => {\n const newPeriod = value as StudentsHighlightPeriod;\n if (newPeriod === periodRef.current) {\n return;\n }\n const effectiveScoreType = scoreTypeRef.current;\n\n setSearchParams((prev) => {\n const newParams = new URLSearchParams(prev);\n if (newPeriod === '1_MONTH') {\n newParams.delete('period');\n } else {\n newParams.set('period', newPeriod);\n }\n if (effectiveScoreType === 'percentage') {\n newParams.delete('scoreType');\n } else {\n newParams.set('scoreType', effectiveScoreType);\n }\n return newParams;\n });\n\n // Reload aggregated overview\n loadAggregatedOverviewData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current,\n false,\n newPeriod,\n effectiveScoreType\n );\n\n // Reload data with new period\n if (simulatedViewTabRef.current === SimulatedViewTab.STUDENTS) {\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current,\n 1,\n false,\n newPeriod,\n effectiveScoreType\n );\n } else {\n loadSkillsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current,\n newPeriod,\n effectiveScoreType\n );\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current,\n 1,\n false,\n newPeriod,\n effectiveScoreType\n );\n }\n loadGeneralOverviewData(newPeriod, effectiveScoreType);\n },\n [\n setSearchParams,\n loadStudentsData,\n loadSkillsData,\n loadGeneralOverviewData,\n loadAggregatedOverviewData,\n ]\n );\n\n const handleScoreTypeChange = useCallback(\n (value: string) => {\n const newScoreType = value as ScoreType;\n if (newScoreType === scoreTypeRef.current) {\n return;\n }\n const effectivePeriod = periodRef.current;\n setSearchParams((prev) => {\n const newParams = new URLSearchParams(prev);\n if (effectivePeriod === '1_MONTH') {\n newParams.delete('period');\n } else {\n newParams.set('period', effectivePeriod);\n }\n if (newScoreType === 'percentage') {\n newParams.delete('scoreType');\n } else {\n newParams.set('scoreType', newScoreType);\n }\n return newParams;\n });\n },\n [setSearchParams]\n );\n\n const handleAreaKnowledgeChange = useCallback(\n (areaId: string | null) => {\n if (areaId === selectedAreaKnowledgeIdRef.current) {\n return;\n }\n\n // Find the area to get relatedIds (for merged areas with same name)\n const area = generalOverviewData?.areas.find((a) => a.id === areaId);\n const relatedIds = area?.relatedIds ?? (areaId ? [areaId] : []);\n\n setSelectedAreaKnowledgeId(areaId);\n setSelectedAreaRelatedIds(relatedIds);\n setSelectedSubjectId(null);\n setSimulatedViewTab(SimulatedViewTab.STUDENTS);\n\n loadAggregatedOverviewData(areaId, null);\n loadStudentsData(areaId, null);\n },\n [loadStudentsData, loadAggregatedOverviewData, generalOverviewData]\n );\n\n const handleSubjectChange = useCallback(\n (subjectId: string | null) => {\n const effectiveSubjectId =\n subjectId === 'all' || subjectId === null ? null : subjectId;\n if (effectiveSubjectId === selectedSubjectIdRef.current) {\n return;\n }\n\n setSelectedSubjectId(effectiveSubjectId);\n\n // Always reload aggregated overview\n loadAggregatedOverviewData(\n selectedAreaKnowledgeIdRef.current,\n effectiveSubjectId\n );\n\n if (simulatedViewTabRef.current === SimulatedViewTab.STUDENTS) {\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n effectiveSubjectId\n );\n } else {\n loadSkillsData(selectedAreaKnowledgeIdRef.current, effectiveSubjectId);\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n effectiveSubjectId\n );\n }\n },\n [loadStudentsData, loadSkillsData, loadAggregatedOverviewData]\n );\n\n const handleViewTabChange = useCallback(\n (value: string) => {\n const newTab = value as SimulatedViewTab;\n\n if (newTab === simulatedViewTabRef.current) {\n return;\n }\n\n setSimulatedViewTab(newTab);\n\n if (newTab === SimulatedViewTab.STUDENTS) {\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n } else {\n loadSkillsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n }\n },\n [loadStudentsData, loadSkillsData]\n );\n\n const handleFiltersApply = useCallback(\n (newFilters: SimulatedFilters) => {\n // Atualizar ref ANTES para que as funções de load usem os novos valores\n filtersRef.current = newFilters;\n setFilters(newFilters);\n\n // Reload aggregated overview\n loadAggregatedOverviewData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n\n if (simulatedViewTabRef.current === SimulatedViewTab.STUDENTS) {\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n } else {\n loadSkillsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n }\n loadGeneralOverviewData();\n },\n [\n loadStudentsData,\n loadSkillsData,\n loadGeneralOverviewData,\n loadAggregatedOverviewData,\n ]\n );\n\n const handleStudentsParamsChange = useCallback(\n (params: Record<string, unknown>) => {\n const newPage = params.page as number;\n const sortBy = params.sortBy as string;\n const sortOrder = params.sortOrder as string;\n\n const sortByMap: Record<string, string> = {\n name: 'name',\n school: 'school',\n schoolYear: 'schoolYear',\n class: 'class',\n average: 'average',\n performance: 'performance',\n };\n\n const orderBy = sortBy ? (sortByMap[sortBy] ?? 'name') : 'name';\n const order = sortOrder === 'desc' ? 'desc' : 'asc';\n const currentAreaKnowledgeId = selectedAreaKnowledgeIdRef.current;\n const currentSubjectId = selectedSubjectIdRef.current;\n const isEssay = currentAreaKnowledgeId === ESSAY_AREA_ID;\n\n const effectiveSubjectId =\n !isEssay && currentSubjectId && currentSubjectId !== 'all'\n ? currentSubjectId\n : undefined;\n const effectiveAreaKnowledgeId =\n !isEssay && currentAreaKnowledgeId && currentAreaKnowledgeId !== 'all'\n ? currentAreaKnowledgeId\n : undefined;\n\n fetchSimulatedOverview(\n {\n simulationType: isEssay ? 'essays' : 'enem-1',\n period: periodRef.current,\n subjectId: effectiveSubjectId,\n areaKnowledgeId: effectiveAreaKnowledgeId,\n schoolIds: filtersRef.current.schoolIds,\n schoolYearIds: filtersRef.current.schoolYearIds,\n classIds: filtersRef.current.classIds,\n studentsIds: filtersRef.current.studentsIds,\n page: newPage ?? 1,\n limit: 10,\n orderBy,\n order,\n scoreType: scoreTypeRef.current,\n },\n true\n );\n },\n [fetchSimulatedOverview]\n );\n\n const handleContentsParamsChange = useCallback(\n (params: Record<string, unknown>) => {\n if (selectedAreaKnowledgeIdRef.current === ESSAY_AREA_ID) {\n return;\n }\n\n const newPage = params.page as number;\n const newLimit = params.limit as number;\n const sortBy = params.sortBy as string;\n const sortOrder = params.sortOrder as string;\n\n const sortByMap: Record<string, string> = {\n contentName: 'contentName',\n simulatedExamsCount: 'simulatedExamsCount',\n questionsCount: 'questionsCount',\n studentsCount: 'studentsCount',\n performance: 'correctPercentage',\n };\n\n const orderBy = sortBy\n ? (sortByMap[sortBy] ?? 'correctPercentage')\n : 'correctPercentage';\n const order = sortOrder === 'desc' ? 'desc' : 'asc';\n\n const currentSubjectId = selectedSubjectIdRef.current;\n const currentAreaKnowledgeId = selectedAreaKnowledgeIdRef.current;\n const effectiveSubjectId =\n currentSubjectId && currentSubjectId !== 'all'\n ? currentSubjectId\n : undefined;\n const effectiveAreaKnowledgeId =\n currentAreaKnowledgeId && currentAreaKnowledgeId !== 'all'\n ? currentAreaKnowledgeId\n : undefined;\n\n fetchContents({\n simulationType: 'enem-1',\n period: periodRef.current,\n subjectId: effectiveSubjectId,\n areaKnowledgeId: effectiveAreaKnowledgeId,\n schoolIds: filtersRef.current.schoolIds,\n schoolYearIds: filtersRef.current.schoolYearIds,\n classIds: filtersRef.current.classIds,\n studentsIds: filtersRef.current.studentsIds,\n page: newPage ?? 1,\n limit: newLimit ?? 10,\n orderBy,\n order,\n scoreType: scoreTypeRef.current,\n });\n },\n [fetchContents]\n );\n\n const handleStudentRowClick = useCallback((row: SimulatedStudentItem) => {\n setSelectedStudent({\n userInstitutionId: row.userInstitutionId,\n name: row.name,\n });\n setIsStudentModalOpen(true);\n }, []);\n\n const handleContentRowClick = useCallback((row: SimulatedContentItem) => {\n setSelectedContent({\n contentId: row.contentId,\n contentName: row.contentName,\n });\n setIsContentModalOpen(true);\n }, []);\n\n // === Carregamento inicial ===\n const initialLoadRef = useRef(false);\n const hasInitialLoadCompleted = useRef(false);\n\n useEffect(() => {\n // Skip if hook is disabled (e.g., not on simulated report)\n if (!enabled) return;\n if (initialLoadRef.current) return;\n initialLoadRef.current = true;\n\n // Load aggregated overview (based on profile)\n loadAggregatedOverviewData(null, null);\n // Load paginated students for table\n loadStudentsData(null, null);\n loadGeneralOverviewData();\n\n // Set after microtask to prevent scoreType effect from firing on mount\n Promise.resolve().then(() => {\n hasInitialLoadCompleted.current = true;\n });\n }, [\n enabled,\n loadStudentsData,\n loadGeneralOverviewData,\n loadAggregatedOverviewData,\n ]);\n\n // === Recarregar quando scoreType muda ===\n useEffect(() => {\n // Skip if hook is disabled\n if (!enabled) return;\n if (!hasInitialLoadCompleted.current) return;\n\n // Always reload aggregated overview\n loadAggregatedOverviewData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n\n if (simulatedViewTabRef.current === SimulatedViewTab.STUDENTS) {\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n } else {\n loadSkillsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n loadStudentsData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n }\n loadGeneralOverviewData();\n }, [\n enabled,\n scoreType,\n loadStudentsData,\n loadSkillsData,\n loadGeneralOverviewData,\n loadAggregatedOverviewData,\n ]);\n\n // === Recarregar quando aggregationType muda (perfil carregado após mount) ===\n const previousAggregationTypeRef = useRef(aggregationType);\n useEffect(() => {\n // Skip if hook is disabled\n if (!enabled) return;\n // Skip if aggregationType hasn't actually changed\n if (previousAggregationTypeRef.current === aggregationType) return;\n previousAggregationTypeRef.current = aggregationType;\n\n // Skip if initial load hasn't completed\n if (!hasInitialLoadCompleted.current) return;\n\n // Reload aggregated overview with new aggregation type\n loadAggregatedOverviewData(\n selectedAreaKnowledgeIdRef.current,\n selectedSubjectIdRef.current\n );\n }, [enabled, aggregationType, loadAggregatedOverviewData]);\n\n return {\n // Query Params\n period,\n scoreType,\n\n // Estados de Seleção\n selectedAreaKnowledgeId,\n selectedAreaRelatedIds,\n selectedSubjectId,\n simulatedViewTab,\n isEssaySelected,\n\n // Filtros\n filters,\n activeFiltersCount,\n\n // Aggregation Type\n aggregationType,\n\n // Dados da API\n generalOverview: {\n data: generalOverviewData,\n loading: generalOverviewLoading,\n error: generalOverviewError,\n },\n aggregatedOverview: {\n data: aggregatedData,\n loading: aggregatedLoading,\n isRefreshing: aggregatedRefreshing,\n error: aggregatedError,\n },\n studentsOverview: {\n data: simulatedData,\n loading: simulatedLoading,\n isRefreshing: simulatedRefreshing,\n error: simulatedError,\n },\n contentsPerformance: {\n data: contentsData,\n loading: contentsLoading,\n isRefreshing: contentsRefreshing,\n error: contentsError,\n },\n\n // Handlers\n handlePeriodChange,\n handleScoreTypeChange,\n handleAreaKnowledgeChange,\n handleSubjectChange,\n handleViewTabChange,\n handleFiltersApply,\n handleStudentsParamsChange,\n handleContentsParamsChange,\n handleStudentRowClick,\n handleContentRowClick,\n\n // Modais\n studentModal: {\n isOpen: isStudentModalOpen,\n student: selectedStudent,\n close: () => {\n setIsStudentModalOpen(false);\n setSelectedStudent(null);\n },\n },\n contentModal: {\n isOpen: isContentModalOpen,\n content: selectedContent,\n close: () => {\n setIsContentModalOpen(false);\n setSelectedContent(null);\n },\n },\n filtersModal: {\n isOpen: isFiltersModalOpen,\n open: () => setIsFiltersModalOpen(true),\n close: () => setIsFiltersModalOpen(false),\n },\n\n // Colunas das Tabelas\n studentsTableColumns,\n contentsTableColumns,\n };\n}\n","import Text from '../Text/Text';\nimport ProgressBar from '../ProgressBar/ProgressBar';\nimport { SkeletonRounded } from '../Skeleton/Skeleton';\nimport IconRender from '../IconRender/IconRender';\nimport { formatScore, hexToRgba } from '../../utils/utils';\nimport { ScoreType } from '../../types/common';\nimport {\n type GeneralOverviewSectionProps,\n type AreaKnowledgePerformance,\n type EssayPerformance,\n} from './types';\n\n/**\n * Area knowledge card component\n * Uses color and icon from API data\n */\nfunction AreaCard({\n area,\n scoreType = ScoreType.PERCENTAGE,\n}: {\n readonly area: AreaKnowledgePerformance;\n readonly scoreType?: ScoreType;\n}) {\n const bgColor = hexToRgba(area.color, 0.1);\n\n return (\n <div\n className=\"flex-1 min-w-0 flex flex-col items-center justify-center p-4 rounded-xl\"\n style={{ backgroundColor: bgColor }}\n >\n {/* Icon with rounded background */}\n <div\n className=\"w-8 h-8 flex items-center justify-center rounded-full mb-2\"\n style={{ backgroundColor: area.color }}\n >\n <IconRender iconName={area.icon} size={18} color=\"white\" />\n </div>\n\n {/* Full name (uppercase, smaller) */}\n <Text\n size=\"xs\"\n weight=\"semibold\"\n className=\"text-center uppercase text-text-700 mb-1 line-clamp-2\"\n >\n {area.name}\n </Text>\n\n {/* Score (percentage or TRI) */}\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n {formatScore(area.percentage, scoreType)}\n </Text>\n </div>\n );\n}\n\n/**\n * Essay card component\n * Displays essay performance using color and icon from API\n * Note: Essays always use percentage, TRI doesn't apply to essays\n */\nfunction EssayCard({ essay }: { readonly essay: EssayPerformance }) {\n const bgColor = hexToRgba(essay.color, 0.1);\n\n return (\n <div\n className=\"flex-1 min-w-0 flex flex-col items-center justify-center p-4 rounded-xl\"\n style={{ backgroundColor: bgColor }}\n >\n {/* Icon with rounded background */}\n <div\n className=\"w-8 h-8 flex items-center justify-center rounded-full mb-2\"\n style={{ backgroundColor: essay.color }}\n >\n <IconRender iconName={essay.icon} size={18} color=\"white\" />\n </div>\n\n {/* Name (uppercase, smaller) */}\n <Text\n size=\"xs\"\n weight=\"semibold\"\n className=\"text-center uppercase text-text-700 mb-1 line-clamp-2\"\n >\n {essay.name}\n </Text>\n\n {/* Percentage large - Essays always use percentage */}\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n {formatScore(essay.percentage, ScoreType.PERCENTAGE)}\n </Text>\n </div>\n );\n}\n\n/**\n * Skeleton loader for area cards\n */\nfunction AreaCardSkeleton() {\n return (\n <div className=\"flex-1 min-w-0 flex flex-col items-center justify-center p-4 rounded-xl bg-background-50\">\n <SkeletonRounded className=\"h-8 w-8 rounded-full mb-2\" />\n <SkeletonRounded className=\"h-3 w-16 mb-1\" />\n <SkeletonRounded className=\"h-6 w-12\" />\n </div>\n );\n}\n\n/**\n * GeneralOverviewSection - General overview section for simulated exams\n *\n * Displays overall proficiency percentage and performance by area of knowledge.\n * Uses color and icon directly from API data for each area.\n * This section is independent of areaKnowledgeId and subjectId filters -\n * it always shows consolidated data across all simulated exams.\n *\n * @example\n * ```tsx\n * const { data, loading, error, fetchOverview } = useGeneralOverview(api);\n *\n * <GeneralOverviewSection\n * data={data}\n * loading={loading}\n * error={error}\n * scoreType=\"tri\"\n * />\n * ```\n */\nexport function GeneralOverviewSection({\n data,\n loading = false,\n error = null,\n scoreType = ScoreType.PERCENTAGE,\n}: Readonly<GeneralOverviewSectionProps>) {\n if (error) {\n return (\n <div className=\"p-4 bg-error-50 border border-error-200 rounded-xl\">\n <Text size=\"sm\" className=\"text-error-700\">\n {error}\n </Text>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"space-y-4 p-5 bg-background border border-border-50 rounded-xl\">\n {/* Header */}\n <div className=\"space-y-1\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Geral\n </Text>\n <SkeletonRounded className=\"h-4 w-3/4\" />\n </div>\n\n {/* Overall progress skeleton */}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex-1\">\n <SkeletonRounded className=\"h-3 w-full rounded-full\" />\n </div>\n <SkeletonRounded className=\"h-6 w-12\" />\n </div>\n\n {/* Area cards skeleton */}\n <div className=\"flex gap-3\">\n {[1, 2, 3, 4, 5].map((i) => (\n <AreaCardSkeleton key={i} />\n ))}\n </div>\n </div>\n );\n }\n\n if (!data) {\n return null;\n }\n\n return (\n <div className=\"space-y-4 p-5 bg-background border border-border-50 rounded-xl\">\n {/* Header with description */}\n <div className=\"space-y-1\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Geral\n </Text>\n <Text size=\"sm\" className=\"text-text-500\">\n Dados que mostram a proficiência de todos os simulados digitais já\n realizados em todas as áreas do conhecimento\n </Text>\n </div>\n\n {/* Overall progress bar */}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex-1\">\n <ProgressBar\n value={\n scoreType === ScoreType.TRI\n ? data.overallPercentage / 10\n : data.overallPercentage\n }\n variant=\"green\"\n size=\"small\"\n />\n </div>\n <Text\n size=\"lg\"\n weight=\"bold\"\n className=\"text-text-950 min-w-[50px] text-right\"\n >\n {formatScore(data.overallPercentage, scoreType)}\n </Text>\n </div>\n\n {/* Area knowledge cards + Essay card - flex to fill 100% equally */}\n {(data.areas.length > 0 || data.essay) && (\n <div className=\"flex gap-3\">\n {data.areas.map((area) => (\n <AreaCard key={area.id} area={area} scoreType={scoreType} />\n ))}\n {data.essay && <EssayCard essay={data.essay} />}\n </div>\n )}\n </div>\n );\n}\n\nexport default GeneralOverviewSection;\n","import { useState, useCallback, useRef } from 'react';\nimport type { BaseApiClient } from '../../types/api';\nimport type {\n GeneralOverviewParams,\n GeneralOverviewApiResponse,\n UseGeneralOverviewState,\n UseGeneralOverviewReturn,\n} from './types';\n\n/**\n * Initial state for the hook\n */\nconst initialState: UseGeneralOverviewState = {\n data: null,\n loading: false,\n isRefreshing: false,\n error: null,\n};\n\n/**\n * Hook for fetching general overview data for simulated exams\n *\n * Receives the API client and calls the endpoint directly.\n * Endpoint: POST /performance/simulated/general-overview\n *\n * @param api - API client instance (axios-like)\n * @returns Hook state and methods\n *\n * @example\n * ```tsx\n * import api from '@/services/apiService';\n *\n * const { data, loading, error, fetchOverview } = useGeneralOverview(api);\n *\n * useEffect(() => {\n * fetchOverview({\n * period: '1_MONTH',\n * schoolIds: ['school-id'],\n * scoreType: 'tri',\n * });\n * }, []);\n * ```\n */\nexport function useGeneralOverview(\n api: BaseApiClient\n): UseGeneralOverviewReturn {\n const [state, setState] = useState<UseGeneralOverviewState>(initialState);\n const requestIdRef = useRef(0);\n\n /**\n * Fetch general overview data\n * @param params - Request parameters\n * @param refresh - If true, sets isRefreshing instead of loading\n */\n const fetchOverview = useCallback(\n async (params: GeneralOverviewParams, refresh = false) => {\n const currentRequestId = ++requestIdRef.current;\n\n // Set loading or refreshing state\n setState((prev) => ({\n ...prev,\n loading: !refresh,\n isRefreshing: refresh,\n error: null,\n }));\n\n try {\n // Build URL with optional scoreType query param\n let url = '/performance/simulated/general-overview';\n if (params.scoreType && params.scoreType !== 'percentage') {\n url += `?scoreType=${params.scoreType}`;\n }\n\n // Build request body (excludes scoreType and studentsIds)\n const body = {\n period: params.period,\n schoolIds: params.schoolIds || [],\n schoolYearIds: params.schoolYearIds || [],\n classIds: params.classIds || [],\n };\n\n const response = await api.post<GeneralOverviewApiResponse>(url, body);\n\n // Ignore stale responses\n if (currentRequestId !== requestIdRef.current) return;\n\n setState({\n data: response.data.data,\n loading: false,\n isRefreshing: false,\n error: null,\n });\n } catch (err) {\n // Ignore errors from stale requests\n if (currentRequestId !== requestIdRef.current) return;\n\n const errorMessage =\n err instanceof Error\n ? err.message\n : 'Erro ao carregar dados gerais de desempenho';\n\n console.error('Error fetching general overview:', err);\n\n setState((prev) => ({\n ...prev,\n loading: false,\n isRefreshing: false,\n error: errorMessage,\n }));\n }\n },\n [api]\n );\n\n /**\n * Reset state to initial values\n */\n const reset = useCallback(() => {\n // Invalidate any in-flight requests\n requestIdRef.current++;\n setState(initialState);\n }, []);\n\n return {\n ...state,\n fetchOverview,\n reset,\n };\n}\n\nexport default useGeneralOverview;\n","import { useState, useCallback, useRef } from 'react';\nimport type { BaseApiClient } from '../../types/api';\nimport type {\n SimulatedOverviewData,\n SimulatedOverviewParams,\n SimulatedOverviewApiResponse,\n UseSimulatedOverviewState,\n UseSimulatedOverviewReturn,\n} from './types';\nimport {\n simulationTypeToActivityFilters,\n SimulationType,\n} from '../SimulatedStudentDetailsModal/types';\nimport { getErrorMessage } from './utils';\n\n/**\n * Body type for POST endpoints\n */\ninterface SimulatedOverviewBody {\n period?: string;\n subjectId?: string;\n areaKnowledgeId?: string;\n schoolIds?: string[];\n schoolYearIds?: string[];\n classIds?: string[];\n studentsIds?: string[];\n page?: number;\n limit?: number;\n orderBy?: string;\n order?: 'asc' | 'desc';\n}\n\n/**\n * Build the API endpoint with activity filters and scoreType as query params\n */\nfunction buildEndpoint(\n simulationType: SimulationType,\n scoreType?: string\n): string {\n // Essays still use the legacy endpoint since they don't have activity filters\n if (simulationType === 'essays') {\n const params = new URLSearchParams();\n if (scoreType && scoreType !== 'percentage') {\n params.append('scoreType', scoreType);\n }\n const queryString = params.toString();\n const endpoint = '/performance/simulated/essays/students-overview';\n\n if (!queryString) {\n return endpoint;\n }\n\n return `${endpoint}?${queryString}`;\n }\n\n const activityFilters = simulationTypeToActivityFilters(simulationType);\n const params = new URLSearchParams();\n\n activityFilters.types?.forEach((t) => params.append('types', t));\n // Note: subtypes are not sent - the backend filters by areaKnowledgeId/subjectId in the body\n activityFilters.statuses?.forEach((s) => params.append('statuses', s));\n\n // Add scoreType if not default (percentage)\n if (scoreType && scoreType !== 'percentage') {\n params.append('scoreType', scoreType);\n }\n\n const queryString = params.toString();\n const endpoint = '/performance/simulated/activities/students-overview';\n\n if (!queryString) {\n return endpoint;\n }\n\n return `${endpoint}?${queryString}`;\n}\n\n/**\n * Initial state for the hook\n */\nconst initialState: UseSimulatedOverviewState = {\n data: null,\n loading: false,\n isRefreshing: false,\n error: null,\n};\n\n/**\n * Hook for fetching simulated exams overview data\n * Supports ENEM Prova 1, ENEM Prova 2, and Essays\n *\n * @param api - API client with post method\n *\n * @example\n * ```tsx\n * const { data, loading, error, fetchOverview } = useSimulatedOverview(api);\n *\n * fetchOverview({\n * simulationType: 'enem-1',\n * period: '1_MONTH',\n * subjectId: 'subject-uuid',\n * page: 1,\n * limit: 10,\n * });\n * ```\n */\nexport function useSimulatedOverview(\n api: BaseApiClient\n): UseSimulatedOverviewReturn {\n const [data, setData] = useState<SimulatedOverviewData | null>(\n initialState.data\n );\n const [loading, setLoading] = useState(initialState.loading);\n const [isRefreshing, setIsRefreshing] = useState(initialState.isRefreshing);\n const [error, setError] = useState<string | null>(initialState.error);\n\n // Track request ID to ignore stale responses\n const requestIdRef = useRef(0);\n\n /**\n * Convert params to POST body format\n */\n const paramsToBody = (\n params: SimulatedOverviewParams\n ): SimulatedOverviewBody => {\n return {\n period: params.period,\n subjectId: params.subjectId,\n areaKnowledgeId: params.areaKnowledgeId,\n schoolIds: params.schoolIds,\n schoolYearIds: params.schoolYearIds,\n classIds: params.classIds,\n studentsIds: params.studentsIds,\n page: params.page ?? 1,\n limit: params.limit ?? 10,\n orderBy: params.orderBy ?? 'name',\n order: params.order ?? 'asc',\n };\n };\n\n const fetchOverview = useCallback(\n async (params: SimulatedOverviewParams, refresh = false) => {\n const currentRequestId = ++requestIdRef.current;\n\n try {\n if (refresh) {\n setIsRefreshing(true);\n } else {\n setLoading(true);\n }\n setError(null);\n\n const endpoint = buildEndpoint(params.simulationType, params.scoreType);\n const body = paramsToBody(params);\n\n const response = await api.post<SimulatedOverviewApiResponse>(\n endpoint,\n body\n );\n\n // Ignore stale responses\n if (currentRequestId !== requestIdRef.current) return;\n\n setData(response.data.data);\n } catch (err) {\n // Ignore errors from stale requests\n if (currentRequestId !== requestIdRef.current) return;\n\n setError(\n getErrorMessage(\n err,\n 'Não foi possível carregar os dados de simulados'\n )\n );\n } finally {\n // Only update loading state for current request\n if (currentRequestId === requestIdRef.current) {\n setLoading(false);\n setIsRefreshing(false);\n }\n }\n },\n [api]\n );\n\n const reset = useCallback(() => {\n // Invalidate any in-flight requests\n requestIdRef.current++;\n setData(initialState.data);\n setLoading(initialState.loading);\n setIsRefreshing(initialState.isRefreshing);\n setError(initialState.error);\n }, []);\n\n return { data, loading, isRefreshing, error, fetchOverview, reset };\n}\n","import type { BaseApiClient } from '../../types/api';\n\n/**\n * Performance tag for simulated exams\n */\nexport enum SimulatedPerformanceTag {\n HIGHLIGHT = 'HIGHLIGHT',\n ABOVE_AVERAGE = 'ABOVE_AVERAGE',\n BELOW_AVERAGE = 'BELOW_AVERAGE',\n ATTENTION_POINT = 'ATTENTION_POINT',\n}\n\n/**\n * Tag config for display\n */\nexport enum SimulatedPerformanceTagVariant {\n SUCCESS = 'success',\n WARNING = 'warning',\n ERROR = 'error',\n INFO = 'info',\n}\n\n/**\n * Tag config for display\n */\nexport interface SimulatedPerformanceTagConfig {\n label: string;\n variant: SimulatedPerformanceTagVariant;\n}\n\n/**\n * Map performance to tag config\n */\nexport const SIMULATED_PERFORMANCE_TAG_CONFIG: Record<\n SimulatedPerformanceTag,\n SimulatedPerformanceTagConfig\n> = {\n [SimulatedPerformanceTag.HIGHLIGHT]: {\n label: 'Destaque da turma',\n variant: SimulatedPerformanceTagVariant.SUCCESS,\n },\n [SimulatedPerformanceTag.ABOVE_AVERAGE]: {\n label: 'Acima da média',\n variant: SimulatedPerformanceTagVariant.INFO,\n },\n [SimulatedPerformanceTag.BELOW_AVERAGE]: {\n label: 'Abaixo da média',\n variant: SimulatedPerformanceTagVariant.WARNING,\n },\n [SimulatedPerformanceTag.ATTENTION_POINT]: {\n label: 'Ponto de atenção',\n variant: SimulatedPerformanceTagVariant.ERROR,\n },\n};\n\n/**\n * Badge action enum for performance status indicators\n */\nexport enum PerformanceBadgeAction {\n SUCCESS = 'success',\n INFO = 'info',\n WARNING = 'warning',\n ERROR = 'error',\n}\n\n/**\n * Map performance tag to Badge action type\n */\nexport const PERFORMANCE_TAG_TO_BADGE_ACTION: Record<\n SimulatedPerformanceTag,\n PerformanceBadgeAction\n> = {\n [SimulatedPerformanceTag.HIGHLIGHT]: PerformanceBadgeAction.SUCCESS,\n [SimulatedPerformanceTag.ABOVE_AVERAGE]: PerformanceBadgeAction.INFO,\n [SimulatedPerformanceTag.BELOW_AVERAGE]: PerformanceBadgeAction.WARNING,\n [SimulatedPerformanceTag.ATTENTION_POINT]: PerformanceBadgeAction.ERROR,\n};\n\n/**\n * Simulation type used in reports\n */\nexport enum ReportSimulationType {\n ENEM_1 = 'enem-1',\n ENEM_2 = 'enem-2',\n ESSAYS = 'essays',\n}\n\n/**\n * Backward-compatible simulation type alias\n */\nexport type SimulationType = `${ReportSimulationType}`;\n\n/**\n * Student info for details modal\n */\nexport interface StudentDetailsInfo {\n studentId: string;\n instituti