analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 19.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/PerformanceReportModal/PerformanceReportModal.tsx"],"sourcesContent":["import { type ReactNode } from 'react';\nimport Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport { MetricBox } from '../shared/MetricBox';\nimport { LegendPieCard, type PieSlice } from '../shared/ChartComponents';\nimport {\n SectionTitle,\n UserHeader,\n ErrorContent,\n} from '../shared/ModalComponents';\nimport { REPORT_PERIOD, REPORT_MODAL_VARIANT } from '../../types/common';\nimport { PERFORMANCE_TAG_CONFIG } from '../../types/performance';\n\n// ─── API Types (match backend exactly) ───────────────────────\n\n/**\n * Query params for GET /performance/user/:userId\n */\nexport interface UserPerformanceRequest {\n userId: string;\n period: REPORT_PERIOD;\n classId?: string;\n}\n\n/**\n * Consolidated question statistics block.\n * Mirrors backend ConsolidatedQuestionStats.\n *\n * Note: `totalAnswered = correctQuestions + incorrectQuestions` (blank excluded).\n * To display the full total use `correctQuestions + incorrectQuestions + blankQuestions`.\n */\nexport interface UserPerformanceQuestionStats {\n totalAnswered: number;\n correctQuestions: number;\n correctPercentage: number;\n incorrectQuestions: number;\n incorrectPercentage: number;\n blankQuestions: number;\n blankPercentage: number;\n /** Localized label e.g. \"Acima da média\", \"Abaixo da média\" */\n performanceTag: string;\n}\n\n/**\n * Downloaded lesson entry.\n * Mirrors backend DownloadedLesson.\n */\nexport interface UserPerformanceLesson {\n lessonId: string;\n lessonName: string;\n bnccCode: string | null;\n}\n\n/**\n * Full 200 response for STUDENT profile.\n * Mirrors backend StudentUserPerformance.\n */\nexport interface UserPerformanceStudentData {\n user: {\n id: string;\n name: string;\n };\n school: {\n schoolId: string;\n schoolName: string;\n };\n class: {\n classId: string;\n className: string;\n };\n schoolYear: {\n schoolYearId: string;\n schoolYearName: string;\n };\n /** Derived from generalStats.performanceTag e.g. \"Abaixo da média\" */\n status: string;\n generalStats: UserPerformanceQuestionStats;\n activityStats: UserPerformanceQuestionStats;\n questionnaireStats: UserPerformanceQuestionStats;\n simulationStats: UserPerformanceQuestionStats;\n downloadedLessons: UserPerformanceLesson[];\n}\n\n/**\n * Consolidated material production stats.\n * Mirrors backend ConsolidatedMaterialStats.\n */\nexport interface UserPerformanceMaterialStats {\n totalMaterialProduced: number;\n totalRecommendedLessons: number;\n recommendedLessonsPercentage: number;\n totalActivities: number;\n activitiesPercentage: number;\n}\n\n/**\n * Full 200 response for TEACHER / UNIT_MANAGER / REGIONAL_MANAGER profiles.\n * Mirrors backend ProfessionalUserPerformance.\n *\n * Note: no user/school/class context is returned for professional profiles.\n * Pass user display info via `professionalUserInfo` if needed.\n */\nexport interface UserPerformanceProfessionalData {\n generalStats: UserPerformanceMaterialStats;\n}\n\n/** @deprecated Use {@link REPORT_MODAL_VARIANT} instead. Re-exported for backwards compatibility. */\nexport { REPORT_MODAL_VARIANT as PerformanceReportModalVariant } from '../../types/common';\n\ninterface PerformanceReportModalBaseProps {\n isOpen: boolean;\n onClose: () => void;\n /** Modal title (default: \"Desempenho em 1 ano\") */\n title?: string;\n loading?: boolean;\n error?: string | null;\n}\n\ntype PerformanceReportModalStudentProps = {\n variant: REPORT_MODAL_VARIANT.STUDENT;\n data: UserPerformanceStudentData | null;\n /**\n * Optional icon mapper for the downloaded lessons table.\n * The API does not return icons; the parent can derive them from lesson data.\n */\n getLessonIcon?: (lesson: UserPerformanceLesson) => ReactNode;\n /**\n * Optional activity/questionnaire/simulation done counts.\n * These are not part of the API response — pass them from other data sources.\n */\n studentActivityCounts?: {\n activities?: number;\n questionnaires?: number;\n simulations?: number;\n };\n professionalUserInfo?: never;\n};\n\ntype PerformanceReportModalProfessionalProps = {\n variant: REPORT_MODAL_VARIANT.PROFESSIONAL;\n data: UserPerformanceProfessionalData | null;\n /**\n * Display info for the professional modal header.\n * The professional API response does not include user/school context,\n * so the parent should supply this from the table row data.\n */\n professionalUserInfo?: {\n userName: string;\n schoolName: string;\n className: string;\n year: string | number;\n };\n getLessonIcon?: never;\n studentActivityCounts?: never;\n};\n\nexport type PerformanceReportModalProps = PerformanceReportModalBaseProps &\n (\n | PerformanceReportModalStudentProps\n | PerformanceReportModalProfessionalProps\n );\n\n// ─── Status → Badge variant mapping ──────────────────────────\n\nfunction getStatusVariant(\n status: string\n): 'success' | 'info' | 'warning' | 'error' {\n return (\n Object.values(PERFORMANCE_TAG_CONFIG).find((c) => c.label === status)\n ?.variant ?? 'info'\n );\n}\n\n// ─── Pie slice builders ───────────────────────────────────────\n\nfunction buildQuestionsSlices(stats: UserPerformanceQuestionStats): PieSlice[] {\n return [\n {\n label: 'Questões corretas',\n value: stats.correctQuestions,\n colorClass: 'bg-success-200',\n },\n {\n label: 'Questões incorretas',\n value: stats.incorrectQuestions,\n colorClass: 'bg-warning-400',\n },\n {\n label: 'Questões em branco',\n value: stats.blankQuestions,\n colorClass: 'bg-background-300',\n },\n ];\n}\n\nfunction buildMaterialSlices(stats: UserPerformanceMaterialStats): PieSlice[] {\n return [\n {\n label: 'Aulas recomendadas',\n value: stats.totalRecommendedLessons,\n colorClass: 'bg-warning-300',\n color: 'var(--Indicator-Indicator-Positive, #F8CC2E)',\n },\n {\n label: 'Atividades',\n value: stats.totalActivities,\n colorClass: 'bg-success-700',\n color: 'var(--Success-success700, #206F3E)',\n },\n ];\n}\n\n// ─── Sub-components ──────────────────────────────────────────\n\n/**\n * A questions stats section: one or two metrics + legend + pie chart.\n * Total displayed = correctQuestions + incorrectQuestions + blankQuestions\n * (backend totalAnswered excludes blank questions).\n * When `count` + `countLabel` are provided, renders two MetricBoxes side by side.\n */\nconst QuestionsSection = ({\n title,\n totalLabel,\n stats,\n count,\n countLabel,\n}: {\n title: string;\n totalLabel: string;\n stats: UserPerformanceQuestionStats;\n count?: number;\n countLabel?: string;\n}) => {\n const displayTotal =\n stats.correctQuestions + stats.incorrectQuestions + stats.blankQuestions;\n const slices = buildQuestionsSlices(stats);\n\n return (\n <div className=\"flex flex-col gap-3\">\n <SectionTitle>{title}</SectionTitle>\n {count !== undefined && countLabel ? (\n <div className=\"flex gap-3\">\n <MetricBox className=\"flex-1\" label={countLabel} value={count} />\n <MetricBox\n className=\"flex-1\"\n label={totalLabel}\n value={displayTotal}\n />\n </div>\n ) : (\n <MetricBox label={totalLabel} value={displayTotal} />\n )}\n <LegendPieCard slices={slices} />\n </div>\n );\n};\n\nconst LessonsTable = ({\n lessons,\n getLessonIcon,\n}: {\n lessons: UserPerformanceLesson[];\n getLessonIcon?: (lesson: UserPerformanceLesson) => ReactNode;\n}) => (\n <div className=\"flex flex-col gap-3\">\n <SectionTitle>Aulas baixadas</SectionTitle>\n <div className=\"bg-background border border-border-50 rounded-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-2 border-b border-border-100\">\n <Text size=\"xs\" weight=\"bold\" className=\"text-text-600\">\n Aula\n </Text>\n <Text size=\"xs\" weight=\"bold\" className=\"text-text-600\">\n BNCC\n </Text>\n </div>\n {lessons.length === 0 ? (\n <div className=\"flex items-center justify-center py-6\">\n <Text size=\"sm\" className=\"text-text-400\">\n Nenhuma aula baixada\n </Text>\n </div>\n ) : (\n lessons.map((lesson) => (\n <div\n key={lesson.lessonId}\n className=\"flex items-center justify-between px-4 py-3 border-b border-border-50 last:border-b-0\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n {getLessonIcon && (\n <span className=\"shrink-0 [&>svg]:w-5 [&>svg]:h-5\">\n {getLessonIcon(lesson)}\n </span>\n )}\n <Text size=\"sm\" className=\"text-text-950 truncate\">\n {lesson.lessonName}\n </Text>\n </div>\n <Text size=\"sm\" className=\"text-text-500 shrink-0 ml-3\">\n {lesson.bnccCode ?? '—'}\n </Text>\n </div>\n ))\n )}\n </div>\n </div>\n);\n\n// ─── Modal content ────────────────────────────────────────────\n\nconst StudentModalContent = ({\n data,\n getLessonIcon,\n activityCounts,\n}: {\n data: UserPerformanceStudentData;\n getLessonIcon?: (lesson: UserPerformanceLesson) => ReactNode;\n activityCounts?: PerformanceReportModalProps['studentActivityCounts'];\n}) => (\n <div className=\"flex flex-col gap-6\">\n <UserHeader\n name={data.user.name}\n school={data.school.schoolName}\n className={data.class.className}\n year={data.schoolYear.schoolYearName}\n statusBadge={\n data.status ? (\n <Badge\n variant=\"solid\"\n action={getStatusVariant(data.status)}\n size=\"small\"\n >\n {data.status.toUpperCase()}\n </Badge>\n ) : undefined\n }\n />\n\n <QuestionsSection\n title=\"Dados gerais\"\n totalLabel=\"Total de questões respondidas\"\n stats={data.generalStats}\n />\n\n <QuestionsSection\n title=\"Dados de atividades\"\n totalLabel=\"Total de questões\"\n stats={data.activityStats}\n count={activityCounts?.activities}\n countLabel=\"Atividades realizadas\"\n />\n\n <QuestionsSection\n title=\"Dados de questionários\"\n totalLabel=\"Total de questões\"\n stats={data.questionnaireStats}\n count={activityCounts?.questionnaires}\n countLabel=\"Questionários realizados\"\n />\n\n <QuestionsSection\n title=\"Dados de simulados\"\n totalLabel=\"Total de questões\"\n stats={data.simulationStats}\n count={activityCounts?.simulations}\n countLabel=\"Simulados realizados\"\n />\n\n <LessonsTable\n lessons={data.downloadedLessons}\n getLessonIcon={getLessonIcon}\n />\n </div>\n);\n\nconst ProfessionalModalContent = ({\n data,\n userInfo,\n}: {\n data: UserPerformanceProfessionalData;\n userInfo?: PerformanceReportModalProps['professionalUserInfo'];\n}) => {\n const slices = buildMaterialSlices(data.generalStats);\n\n return (\n <div className=\"flex flex-col gap-6\">\n {userInfo && (\n <UserHeader\n name={userInfo.userName}\n school={userInfo.schoolName}\n className={userInfo.className}\n year={userInfo.year}\n />\n )}\n\n <div className=\"flex flex-col gap-3\">\n <SectionTitle>Dados gerais</SectionTitle>\n <MetricBox\n label=\"Total de material produzido\"\n value={data.generalStats.totalMaterialProduced}\n />\n <LegendPieCard slices={slices} />\n </div>\n </div>\n );\n};\n\nconst LoadingSkeleton = () => (\n <div className=\"flex flex-col gap-4 animate-pulse\">\n <div className=\"h-16 bg-background-200 rounded-xl\" />\n <div className=\"h-5 bg-background-200 rounded w-32 mt-2\" />\n <div className=\"h-12 bg-background-200 rounded-xl\" />\n <div className=\"h-32 bg-background-200 rounded-xl\" />\n </div>\n);\n\n// ─── Main component ───────────────────────────────────────────\n\n/**\n * PerformanceReportModal component\n *\n * Displays a modal with performance data for a user from `GET /performance/user/:userId`.\n *\n * Two variants driven by the profile type:\n * - `STUDENT` — question stats per section (generalStats, activityStats,\n * questionnaireStats, simulationStats) + downloaded lessons table\n * - `PROFESSIONAL` — material production stats (totalMaterialProduced,\n * totalRecommendedLessons, totalActivities)\n *\n * The professional API response does not include user/school context.\n * Pass `professionalUserInfo` (from the table row) to show a user header.\n *\n * @example\n * ```tsx\n * // Student variant — pass API response directly\n * <PerformanceReportModal\n * isOpen={isOpen}\n * onClose={() => setIsOpen(false)}\n * variant={PerformanceReportModalVariant.STUDENT}\n * data={apiResponse.data}\n * />\n *\n * // Professional variant — API has no user context, pass it separately\n * <PerformanceReportModal\n * isOpen={isOpen}\n * onClose={() => setIsOpen(false)}\n * variant={PerformanceReportModalVariant.PROFESSIONAL}\n * data={apiResponse.data}\n * professionalUserInfo={{\n * userName: row.userName,\n * schoolName: row.schoolName,\n * className: row.className,\n * year: row.year,\n * }}\n * />\n * ```\n */\nexport const PerformanceReportModal = ({\n isOpen,\n onClose,\n title = 'Desempenho em 1 ano',\n loading = false,\n error = null,\n ...variantProps\n}: PerformanceReportModalProps) => {\n let content: ReactNode;\n\n if (loading) {\n content = <LoadingSkeleton />;\n } else if (error) {\n content = <ErrorContent message={error} />;\n } else if (variantProps.data !== null) {\n if (variantProps.variant === REPORT_MODAL_VARIANT.STUDENT) {\n content = (\n <StudentModalContent\n data={variantProps.data}\n getLessonIcon={variantProps.getLessonIcon}\n activityCounts={variantProps.studentActivityCounts}\n />\n );\n } else {\n content = (\n <ProfessionalModalContent\n data={variantProps.data}\n userInfo={variantProps.professionalUserInfo}\n />\n );\n }\n }\n\n return (\n <Modal\n isOpen={isOpen}\n onClose={onClose}\n title={title}\n size=\"lg\"\n contentClassName=\"max-h-[80vh] overflow-y-auto\"\n >\n {content}\n </Modal>\n );\n};\n\nexport default PerformanceReportModal;\n"],"mappings":"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","names":[]}