analytica-frontend-lib 1.6.67 → 1.6.69
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/EnemMoment/index.d.ts +1 -1
- package/dist/EnemMoment/index.d.ts.map +1 -1
- package/dist/EnemMoment/index.js +8 -7
- package/dist/EnemMoment/index.js.map +1 -1
- package/dist/EnemMoment/index.mjs +8 -7
- package/dist/EnemMoment/index.mjs.map +1 -1
- package/dist/components/EnemMoment/HeaderFilters.d.ts.map +1 -1
- package/dist/components/EnemMoment/StudentsTable.d.ts +7 -9
- package/dist/components/EnemMoment/StudentsTable.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ export { EnemMomentSummaryCards, buildEnemMomentUnitSummaryCards, } from '../com
|
|
|
18
18
|
export { MomentTabs, type EnemMomentTab } from '../components/EnemMoment/MomentTabs';
|
|
19
19
|
export { EnemMomentHeaderFilters } from '../components/EnemMoment/HeaderFilters';
|
|
20
20
|
export { ToneTile, answerTiles, numberedExamScores, scoreText, type ToneTileData, } from '../components/EnemMoment/ToneTiles';
|
|
21
|
-
export { StudentsTableSection, createStudentColumns, participationOf, toStudentsTableFilters, toStudentsTableQuery, tookAnyExam, type EnemMomentStudentsTableFilters, type EnemMomentStudentsTableQuery,
|
|
21
|
+
export { StudentsTableSection, createStudentColumns, participationOf, toStudentsTableFilters, toStudentsTableQuery, tookAnyExam, type EnemMomentStudentsTableFilters, type EnemMomentStudentsTableQuery, } from '../components/EnemMoment/StudentsTable';
|
|
22
22
|
export { ENEM_MOMENT_EXAM_STATUS_LABELS, StudentsTableDownload, narrowStudentsExport, studentsExportFileName, studentsExportHeaders, studentsExportRows, studentsExportSheet, } from '../components/EnemMoment/StudentsDownload';
|
|
23
23
|
export { classOptionLabel, listFilter, pickedValue } from '../components/EnemMoment/tableFilters';
|
|
24
24
|
export { PerformanceDistributionSection, toPerformanceCounters, } from '../components/EnemMoment/PerformanceDistributionSection';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/EnemMoment/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,mBAAmB,SAAS,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EACL,8BAA8B,EAC9B,6BAA6B,EAC7B,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,8BAA8B,GACpC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,QAAQ,EACR,OAAO,EACP,iBAAiB,EACjB,QAAQ,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,KAAK,uBAAuB,GAC7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACL,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,WAAW,EACX,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/EnemMoment/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,mBAAmB,SAAS,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EACL,8BAA8B,EAC9B,6BAA6B,EAC7B,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,8BAA8B,GACpC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,QAAQ,EACR,OAAO,EACP,iBAAiB,EACjB,QAAQ,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,KAAK,uBAAuB,GAC7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACL,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,WAAW,EACX,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,8BAA8B,EAC9B,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EACL,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,kCAAkC,CAAC"}
|
package/dist/EnemMoment/index.js
CHANGED
|
@@ -1362,6 +1362,7 @@ function EnemMomentHeaderFilters({
|
|
|
1362
1362
|
{
|
|
1363
1363
|
value: educationStage,
|
|
1364
1364
|
onValueChange: (value) => onEducationStageChange(value),
|
|
1365
|
+
className: "w-auto",
|
|
1365
1366
|
children: [
|
|
1366
1367
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkO4HQL4GLjs.SelectTrigger, { className: "w-auto min-w-45 gap-2", "aria-label": "S\xE9rie", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkO4HQL4GLjs.SelectValue, { placeholder: "S\xE9rie" }) }),
|
|
1367
1368
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkO4HQL4GLjs.SelectContent, { children: EDUCATION_STAGE_OPTIONS.map((option) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkO4HQL4GLjs.SelectItem, { value: option.value, children: option.label }, option.value)) })
|
|
@@ -1669,7 +1670,7 @@ var ORDER_BY = {
|
|
|
1669
1670
|
averageScore: "averageScore"
|
|
1670
1671
|
};
|
|
1671
1672
|
var PARTICIPATION_FILTERS = /* @__PURE__ */ new Set(["PARTICIPATED", "NOT_PARTICIPATED"]);
|
|
1672
|
-
function createStudentColumns(classes
|
|
1673
|
+
function createStudentColumns(classes) {
|
|
1673
1674
|
return [
|
|
1674
1675
|
{
|
|
1675
1676
|
key: "studentName",
|
|
@@ -1725,7 +1726,11 @@ function createStudentColumns(classes, statusColumn = "participation") {
|
|
|
1725
1726
|
label: "Nota",
|
|
1726
1727
|
render: (_value, row) => row.averageScore === null ? _chunkEB63Y4SRjs.MISSING_VALUE : _chunkEB63Y4SRjs.formatScore.call(void 0, row.averageScore)
|
|
1727
1728
|
},
|
|
1728
|
-
|
|
1729
|
+
// Both, in this order: the tier summarises the score beside it, and the
|
|
1730
|
+
// participation is what explains an entirely empty row. Only one used to
|
|
1731
|
+
// show, and which one depended on where the manager had come in from.
|
|
1732
|
+
PERFORMANCE_COLUMN,
|
|
1733
|
+
PARTICIPATION_COLUMN
|
|
1729
1734
|
];
|
|
1730
1735
|
}
|
|
1731
1736
|
var pickedPerformances = (value) => Array.isArray(value) && value.length > 0 ? value : void 0;
|
|
@@ -1771,15 +1776,11 @@ function StudentsTableSection({
|
|
|
1771
1776
|
classes,
|
|
1772
1777
|
onQueryChange,
|
|
1773
1778
|
onStudentClick,
|
|
1774
|
-
statusColumn = "participation",
|
|
1775
1779
|
tableId = "enemMomentStudents",
|
|
1776
1780
|
loadExport,
|
|
1777
1781
|
loadFilteredStudents
|
|
1778
1782
|
}) {
|
|
1779
|
-
const columns = _react.useMemo.call(void 0,
|
|
1780
|
-
() => createStudentColumns(classes, statusColumn),
|
|
1781
|
-
[classes, statusColumn]
|
|
1782
|
-
);
|
|
1783
|
+
const columns = _react.useMemo.call(void 0, () => createStudentColumns(classes), [classes]);
|
|
1783
1784
|
const hasSettled = useHasSettled(loading);
|
|
1784
1785
|
const showSkeleton = loading && (!hasSettled || rows.length === 0);
|
|
1785
1786
|
const isRefreshing = loading && !showSkeleton;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/EnemMoment/index.js","../../src/components/EnemMoment/constants.ts","../../src/components/EnemMoment/SectionContent.tsx","../../src/components/EnemMoment/ExamCards.tsx","../../src/components/EnemMoment/UnitExamDataSection.tsx","../../src/components/EnemMoment/DailyEvolutionSection.tsx","../../src/components/EnemMoment/KnowledgeAreaSection.tsx","../../src/components/EnemMoment/RankingCards.tsx","../../src/components/EnemMoment/SummaryCards.tsx","../../src/components/EnemMoment/MomentTabs.tsx","../../src/components/EnemMoment/HeaderFilters.tsx","../../src/components/EnemMoment/StudentsTable.tsx","../../src/components/EnemMoment/StudentsDownload.tsx","../../src/components/EnemMoment/tableFilters.ts","../../src/components/EnemMoment/PerformanceDistributionSection.tsx"],"names":["count","jsx","jsxs","useState","CaretUpIcon","useMemo","useCallback","useEffect","useRef"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B;AACE;AACA;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACA;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/EnemMoment/index.js","sourcesContent":[null,"import type { EnemMomentEducationStage } from './types';\n\n/**\n * Options of the \"Série\" picker — the teaching stages of the classes. The\n * first is the default: the regular 3rd year is who the exams are sent to.\n */\nexport const EDUCATION_STAGE_OPTIONS: ReadonlyArray<{\n value: EnemMomentEducationStage;\n label: string;\n}> = [\n { value: 'REGULAR', label: '3ª série regular' },\n { value: 'EJA', label: 'EJA' },\n { value: 'SUBSEQUENTE', label: 'Subsequente' },\n];\n\nexport const DEFAULT_EDUCATION_STAGE: EnemMomentEducationStage = 'REGULAR';\n\n/**\n * Options of the \"Tempo de prova\" picker: each the minute its 30-minute range\n * ends at — the backend's `timeBuckets`. Separate ranges, not \"up to\": the\n * labels say both ends so nobody reads \"1h\" as \"até 1h\".\n */\nexport const EXAM_DURATION_OPTIONS: ReadonlyArray<{\n value: number;\n label: string;\n}> = [\n { value: 30, label: 'Até 30 min' },\n { value: 60, label: '30 min a 1h' },\n { value: 90, label: '1h a 1h 30min' },\n { value: 120, label: '1h 30min a 2h' },\n { value: 150, label: '2h a 2h 30min' },\n { value: 180, label: '2h 30min a 3h' },\n { value: 210, label: '3h a 3h 30min' },\n];\n\n/**\n * Color of each moment in the charts, by position: Momento 1 light blue,\n * Momento 2 orange, as in the Figma. Cycles past the fourth.\n */\nexport const MOMENT_COLORS = [\n 'var(--color-info-300)',\n 'var(--color-warning-300)',\n 'var(--color-success-300)',\n 'var(--color-error-300)',\n] as const;\n\n/** \"Detalhes por dia\" rows shown before \"Mostrar todos os dias\". */\nexport const COLLAPSED_DAY_ROWS = 6;\n","import type { ReactNode } from 'react';\nimport Text from '../Text/Text';\nimport { SkeletonCard } from '../Skeleton/Skeleton';\n\n/**\n * A section of the report while it loads (a skeleton of its height), when it\n * failed (the message, in a card of the same height), or its content.\n */\nexport function SectionContent({\n loading,\n error,\n minHeight,\n children,\n}: Readonly<{\n loading: boolean;\n error: string | null;\n /** The section's height class, kept while it loads or fails. */\n minHeight: string;\n children: ReactNode;\n}>) {\n if (loading) return <SkeletonCard className={minHeight} />;\n\n if (error) {\n return (\n <div\n className={`flex items-center justify-center ${minHeight} bg-background border border-border-50 rounded-xl p-5`}\n >\n <Text size=\"sm\" className=\"text-text-500\">\n {error}\n </Text>\n </div>\n );\n }\n\n return <>{children}</>;\n}\n","import type { ReactNode } from 'react';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport { PieChartCard, type PieSlice } from '../shared/ChartComponents';\nimport { ChatCircleIcon } from '@phosphor-icons/react/dist/csr/ChatCircle';\nimport { ClockIcon } from '@phosphor-icons/react/dist/csr/Clock';\nimport { StatCard } from '../StatisticsCard/StatisticsCard';\nimport SimpleBarChart from '../SimpleBarChart/SimpleBarChart';\nimport type {\n EnemMomentExamData,\n EnemMomentParticipation,\n EnemMomentScoreBand,\n} from './types';\nimport { formatCount, formatHoursMinutes, formatMinutesSeconds } from './utils';\n\n/**\n * The cards of the \"Dados do simulado\" block of the Momento ENEM screens: the\n * pie cards (Idioma, Participação), on the lib's PieChartCard, the Tempo card\n * and the score histogram.\n */\n\nconst CARD_CLASS =\n 'flex flex-col min-w-0 p-5 gap-4 bg-background border border-border-50 rounded-xl';\n\nconst plural = (count: number, singular: string, pluralForm: string) =>\n `${formatCount(count)} ${count === 1 ? singular : pluralForm}`;\n\nexport const students = (count: number) =>\n plural(count, 'estudante', 'estudantes');\nexport const schools = (count: number) => plural(count, 'escola', 'escolas');\n\n/** Title, optional icon and the gray line under it — every card's header. */\nfunction CardHeader({\n title,\n subtitle,\n icon,\n}: Readonly<{ title: string; subtitle: string; icon?: ReactNode }>) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-center gap-2 text-text-950\">\n {icon}\n <Text\n as=\"h3\"\n size=\"lg\"\n weight=\"bold\"\n className=\"text-text-950 tracking-[0.2px]\"\n >\n {title}\n </Text>\n </div>\n <Text size=\"sm\" className=\"text-text-600\">\n {subtitle}\n </Text>\n </div>\n );\n}\n\n/**\n * A part of a pie of students, in a CSS color — with a shorter name for the\n * slice's balloon when the legend's does not fit there.\n */\nconst slice = (\n key: string,\n label: string,\n value: number,\n color: string,\n tooltipLabel?: string\n) =>\n ({\n key,\n label,\n value,\n color,\n colorClass: '',\n ...(tooltipLabel === undefined ? {} : { tooltipLabel }),\n }) satisfies PieSlice;\n\n/**\n * Whether the Idioma card shows: the cut has a day 1, with a language to pick.\n * While the exams are not known (`null`), whether somebody picked one.\n */\nexport const showsLanguageCard = (\n hasLanguageChoice: boolean | null,\n language: EnemMomentExamData['language']\n) => hasLanguageChoice ?? language.ingles + language.espanhol > 0;\n\n/** Foreign language picked by the students who took the exam. */\nexport function LanguageCard({\n language,\n}: Readonly<{ language: EnemMomentExamData['language'] }>) {\n return (\n <PieChartCard\n title=\"Idioma\"\n subtitle=\"Língua estrangeira escolhida por quem fez a prova\"\n icon={<ChatCircleIcon size={20} aria-hidden=\"true\" />}\n emptyText=\"Ninguém fez a prova\"\n formatValue={students}\n slices={[\n // The legend has room for \"Escolheram Inglês\"; the slice's balloon\n // does not.\n slice(\n 'english',\n 'Escolheram Inglês',\n language.ingles,\n 'var(--color-info-300)',\n 'Inglês'\n ),\n slice(\n 'spanish',\n 'Escolheram Espanhol',\n language.espanhol,\n 'var(--color-indicator-positive)',\n 'Espanhol'\n ),\n ]}\n />\n );\n}\n\n/**\n * Students who took the exam and who did not. The labels are white with a\n * shadow: black would vanish on the dark \"Não participou\" slice, and the lib's\n * pie takes a single label color for every slice.\n */\nexport function ParticipationCard({\n participation,\n}: Readonly<{ participation: EnemMomentParticipation }>) {\n return (\n <PieChartCard\n title=\"Participação\"\n subtitle=\"Porcentagem de estudantes por participação no simulado\"\n icon={<ChatCircleIcon size={20} aria-hidden=\"true\" />}\n emptyText=\"Nenhum estudante neste recorte\"\n formatValue={students}\n labelColor=\"var(--color-text)\"\n labelTextShadow=\"0 0 3px rgba(0, 0, 0, 0.45)\"\n slices={[\n slice(\n 'participated',\n 'Participou',\n participation.participated,\n 'var(--color-success-200)'\n ),\n slice(\n 'notParticipated',\n 'Não participou',\n participation.notParticipated,\n 'var(--color-indicator-primary)'\n ),\n ]}\n />\n );\n}\n\n/**\n * The three times. Stacked beside other cards; alone on a row (`fullWidth`,\n * a cut with no language choice on the report) the card takes the whole row\n * and they line up.\n *\n * The exam's duration comes with the times only once somebody took it; until\n * then it is the one the exams declare (`examDurationSeconds`).\n */\nexport function TimeCard({\n time,\n examDurationSeconds,\n fullWidth = false,\n}: Readonly<{\n time: EnemMomentExamData['time'];\n examDurationSeconds: number | null;\n fullWidth?: boolean;\n}>) {\n return (\n <div className={cn(CARD_CLASS, fullWidth && 'lg:col-span-2')}>\n <CardHeader\n title=\"Tempo\"\n subtitle=\"Do início ao envio, entre os estudantes que finalizaram\"\n icon={<ClockIcon size={20} aria-hidden=\"true\" />}\n />\n <div\n className={cn('grid grid-cols-1 gap-4', fullWidth && 'md:grid-cols-3')}\n >\n <StatCard\n item={{\n label: 'Tempo médio de prova',\n value: formatHoursMinutes(time.averageTotalSeconds),\n variant: 'total',\n }}\n />\n <StatCard\n item={{\n label: 'Tempo médio por questão',\n value: formatMinutesSeconds(time.averageSecondsPerQuestion),\n variant: 'total',\n }}\n />\n <StatCard\n item={{\n label: 'Duração da prova',\n value: formatHoursMinutes(\n time.durationSeconds ?? examDurationSeconds\n ),\n variant: 'total',\n }}\n />\n </div>\n </div>\n );\n}\n\n/**\n * Participação, Idioma and Tempo side by side — a school's page and the unit\n * report. A cut where nobody picked a language (a day 2 on its own) has no\n * Idioma card, and the other two share the row.\n */\nexport function ExamCardsRow({\n participation,\n examData,\n hasLanguageChoice,\n examDurationSeconds,\n}: Readonly<{\n participation: EnemMomentParticipation;\n examData: EnemMomentExamData;\n /** Whether the cut has a day 1 — `null` while the exams are not known. */\n hasLanguageChoice: boolean | null;\n /** The cut's time limit, from its exams — `null` while they are not known. */\n examDurationSeconds: number | null;\n}>) {\n const withLanguage = showsLanguageCard(hasLanguageChoice, examData.language);\n\n return (\n <div\n className={cn(\n 'grid grid-cols-1 gap-4',\n withLanguage ? 'lg:grid-cols-3' : 'lg:grid-cols-2'\n )}\n >\n <ParticipationCard participation={participation} />\n {withLanguage && <LanguageCard language={examData.language} />}\n <TimeCard\n time={examData.time}\n examDurationSeconds={examDurationSeconds}\n />\n </div>\n );\n}\n\n/** A score histogram, with its total under the title (\"200 estudantes\"). */\nexport function ScoreBandChart({\n title,\n bands,\n formatTotal,\n barColor,\n}: Readonly<{\n title: string;\n bands: EnemMomentScoreBand[];\n formatTotal: (total: number) => string;\n barColor: string;\n}>) {\n const total = bands.reduce((sum, band) => sum + band.count, 0);\n\n return (\n <SimpleBarChart\n title={title}\n subtitle={formatTotal(total)}\n data={bands.map((band) => ({ label: band.label, value: band.count }))}\n barColor={barColor}\n // The axis shows the band alone (\"2 a 4\"), which says neither what it\n // bands nor how many; the balloon has room for the whole sentence.\n formatTooltip={(item) =>\n `Média de ${item.label}: ${formatTotal(item.value)}`\n }\n />\n );\n}\n","import Text from '../Text/Text';\nimport { SectionContent } from './SectionContent';\nimport { ExamCardsRow, ScoreBandChart, students } from './ExamCards';\nimport type {\n EnemMomentExamData,\n EnemMomentSectionState,\n EnemMomentSummary,\n} from './types';\n\n/**\n * \"Dados do simulado\" of the unit report (Gestor de Unidade, professor): how its students scored, then\n * Participação, Idioma and Tempo in a row.\n *\n * One school, so no histogram of school averages; Participação splits the\n * students of the cards, who took the exam and who did not.\n */\nexport function UnitExamDataSection({\n summary,\n examData,\n hasLanguageChoice,\n examDurationSeconds,\n}: Readonly<{\n summary: EnemMomentSectionState<EnemMomentSummary>;\n examData: EnemMomentSectionState<EnemMomentExamData>;\n /** Whether the cut has a day 1 — `null` while the exams are not known. */\n hasLanguageChoice: boolean | null;\n /** The cut's time limit, from its exams — `null` while they are not known. */\n examDurationSeconds: number | null;\n}>) {\n return (\n <section className=\"flex flex-col gap-4\">\n <Text as=\"h2\" size=\"xl\" weight=\"bold\" className=\"text-text-950\">\n Dados do simulado\n </Text>\n <SectionContent\n loading={summary.loading || examData.loading}\n error={summary.error ?? examData.error}\n minHeight=\"min-h-[520px]\"\n >\n {summary.data && examData.data && (\n <div className=\"flex flex-col gap-4\">\n <ScoreBandChart\n title=\"Estudantes por faixa de nota\"\n bands={examData.data.studentScoreBands}\n formatTotal={students}\n barColor=\"bg-info-500\"\n />\n <ExamCardsRow\n participation={{\n participated: summary.data.participatingStudents,\n notParticipated: summary.data.studentsWithoutParticipation,\n }}\n examData={examData.data}\n hasLanguageChoice={hasLanguageChoice}\n examDurationSeconds={examDurationSeconds}\n />\n </div>\n )}\n </SectionContent>\n </section>\n );\n}\n","import { useMemo, useState } from 'react';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport Button from '../Button/Button';\nimport { Tooltip } from '../Tooltip/Tooltip';\nimport {\n YAxis,\n GridLines,\n calculateYAxisTicks,\n} from '../shared/ChartComponents';\nimport {\n TableProvider,\n type ColumnConfig,\n type TableParams,\n} from '../TableProvider/TableProvider';\nimport { CaretDownIcon } from '@phosphor-icons/react/dist/csr/CaretDown';\nimport { CaretUpIcon } from '@phosphor-icons/react/dist/csr/CaretUp';\nimport { SectionContent } from './SectionContent';\nimport { COLLAPSED_DAY_ROWS, MOMENT_COLORS } from './constants';\nimport type {\n EnemMomentDailyEvolution,\n EnemMomentDay,\n EnemMomentMoment,\n EnemMomentSectionState,\n} from './types';\nimport { formatCount, formatScore, MISSING_VALUE } from './utils';\n\nconst CHART_HEIGHT = 180;\n\n/** `YYYY-MM-DD` → `DD/MM`. */\nconst shortDate = (date: string) => {\n const [, month, day] = date.slice(0, 10).split('-');\n return `${day}/${month}`;\n};\n\nconst simulados = (count: number) =>\n `${formatCount(count)} ${count === 1 ? 'simulado' : 'simulados'}`;\n\n/** A moment's color and name, and a fallback for an exam the tabs lack. */\nfunction useMomentLookup(moments: EnemMomentMoment[]) {\n return useMemo(() => {\n const byExam = new Map(\n moments.map((moment, index) => [\n moment.examId,\n { ...moment, color: MOMENT_COLORS[index % MOMENT_COLORS.length] },\n ])\n );\n return (examId: string) =>\n byExam.get(examId) ?? {\n examId,\n label: 'Momento',\n color: MOMENT_COLORS[0],\n };\n }, [moments]);\n}\n\n/**\n * \"Simulados realizados por dia\": one bar per day with submissions, split in\n * the color of each moment submitted that day, with the total and each\n * moment's share above.\n */\nfunction DailyChart({\n days,\n moments,\n}: Readonly<{ days: EnemMomentDay[]; moments: EnemMomentMoment[] }>) {\n const momentOf = useMomentLookup(moments);\n\n const totalsByExam = new Map<string, number>();\n for (const day of days) {\n for (const exam of day.byExam) {\n totalsByExam.set(\n exam.examId,\n (totalsByExam.get(exam.examId) ?? 0) + exam.participations\n );\n }\n }\n const total = days.reduce((sum, day) => sum + day.participations, 0);\n // Only the moments of the cut: a Momento tab lists one.\n const legend = moments.filter((moment) => totalsByExam.has(moment.examId));\n\n const ticks = calculateYAxisTicks(\n Math.max(0, ...days.map((day) => day.participations))\n );\n const max = ticks[0];\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Simulados realizados por dia\n </Text>\n <Badge\n variant=\"solid\"\n action=\"info\"\n size=\"small\"\n className=\"self-start uppercase\"\n >\n {`${simulados(total)} ${total === 1 ? 'total' : 'totais'}`}\n </Badge>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-2\">\n {legend.map((moment) => (\n <div key={moment.examId} className=\"flex items-center gap-2\">\n <span\n className=\"w-2 h-2 rounded-full shrink-0\"\n style={{ backgroundColor: momentOf(moment.examId).color }}\n />\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-950\">\n {moment.label}\n </Text>\n <Text size=\"sm\" className=\"text-text-600\">\n {simulados(totalsByExam.get(moment.examId) ?? 0)}\n </Text>\n </div>\n ))}\n </div>\n\n <div className=\"flex flex-row min-w-0\">\n <YAxis ticks={ticks} chartHeight={CHART_HEIGHT} />\n <div className=\"flex-1 min-w-0 relative\">\n <GridLines ticks={ticks} chartHeight={CHART_HEIGHT} />\n <div className=\"flex flex-row gap-1 sm:gap-2 relative z-10\">\n {days.map((day) => (\n <div\n key={day.day}\n className=\"flex-1 min-w-0 flex flex-col items-center gap-2\"\n >\n <div\n data-testid={`day-bar-${day.day}`}\n aria-label={`${shortDate(day.day)}: ${simulados(day.participations)}`}\n className=\"w-full max-w-8 flex flex-col-reverse rounded-md overflow-hidden\"\n style={{ height: CHART_HEIGHT }}\n >\n {day.byExam\n .filter((exam) => exam.participations > 0)\n .map((exam) => {\n const moment = momentOf(exam.examId);\n const height =\n max === 0\n ? 0\n : (exam.participations / max) * CHART_HEIGHT;\n return (\n <Tooltip\n key={exam.examId}\n content={`${moment.label} · ${simulados(exam.participations)}`}\n position=\"top\"\n className=\"w-full flex\"\n // The bar box clips its content to keep the stacked\n // segments inside one rounded rectangle, and a\n // balloon rendered in place is clipped with them:\n // what reached the screen was a sliver of text as\n // wide as the bar. The portal takes it out of there.\n usePortal\n >\n <div\n data-testid={`day-bar-${day.day}-${exam.examId}`}\n className=\"w-full\"\n style={{\n height: `${height}px`,\n backgroundColor: moment.color,\n }}\n />\n </Tooltip>\n );\n })}\n </div>\n <Text\n size=\"2xs\"\n weight=\"medium\"\n className=\"w-full text-text-600 text-center leading-tight sm:text-xs\"\n >\n {`Dia ${shortDate(day.day)}`}\n </Text>\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\ntype DayRow = {\n rowId: string;\n /** For sorting: the date, `YYYY-MM-DD`. */\n date: string;\n /** \"Dia N · DD/MM\" — what the search matches. */\n label: string;\n dayNumber: number;\n participations: number;\n accumulatedParticipations: number;\n accumulatedAverageScore: number | null;\n};\n\nconst DAY_COLUMNS: ColumnConfig<DayRow>[] = [\n {\n key: 'date',\n label: 'Dia',\n sortable: true,\n className: 'min-w-[200px]',\n render: (_value, row) => (\n <Text size=\"sm\" className=\"text-text-800\">\n {`Dia ${row.dayNumber} · `}\n <Text as=\"span\" size=\"sm\" weight=\"bold\" className=\"text-text-950\">\n {shortDate(row.date)}\n </Text>\n </Text>\n ),\n },\n {\n key: 'participations',\n label: 'Provas realizadas',\n sortable: false,\n render: (_value, row) => formatCount(row.participations),\n },\n {\n key: 'accumulatedParticipations',\n label: 'Acumulado',\n sortable: false,\n render: (_value, row) => formatCount(row.accumulatedParticipations),\n },\n {\n key: 'accumulatedAverageScore',\n label: 'Nota média acumulada',\n sortable: false,\n render: (_value, row) =>\n row.accumulatedAverageScore === null\n ? MISSING_VALUE\n : formatScore(row.accumulatedAverageScore),\n },\n];\n\n/**\n * \"Detalhes por dia\": the days with submissions, searchable and sortable by date,\n * collapsed to the first rows behind \"Mostrar todos os dias\".\n *\n * \"Dia N\" counts the days of the cut in date order, so it does not change when\n * the table is sorted or searched.\n */\nfunction DaysTable({ days }: Readonly<{ days: EnemMomentDay[] }>) {\n const [search, setSearch] = useState('');\n const [order, setOrder] = useState<'asc' | 'desc'>('asc');\n const [expanded, setExpanded] = useState(false);\n\n const allRows = useMemo<DayRow[]>(\n () =>\n days.map((day, index) => ({\n rowId: day.day,\n date: day.day,\n label: `Dia ${index + 1} · ${shortDate(day.day)}`,\n dayNumber: index + 1,\n participations: day.participations,\n accumulatedParticipations: day.accumulatedParticipations,\n accumulatedAverageScore: day.accumulatedAverageScore,\n })),\n [days]\n );\n\n const rows = useMemo(() => {\n const term = search.trim().toLocaleLowerCase('pt-BR');\n const matching = term\n ? allRows.filter((row) =>\n row.label.toLocaleLowerCase('pt-BR').includes(term)\n )\n : allRows;\n return order === 'asc' ? matching : [...matching].reverse();\n }, [allRows, search, order]);\n\n const visibleRows = expanded ? rows : rows.slice(0, COLLAPSED_DAY_ROWS);\n const hasMore = rows.length > COLLAPSED_DAY_ROWS;\n\n // Server mode: the table only reports search and sort, and they are applied\n // here to every day before the collapse cut — not to the rows on screen.\n const handleParamsChange = (params: TableParams) => {\n setSearch(params.search ?? '');\n setOrder(params.sortOrder === 'desc' ? 'desc' : 'asc');\n };\n\n return (\n <div className=\"flex flex-col items-center gap-4\">\n <TableProvider<DayRow>\n tableId=\"enemMomentDays\"\n data={visibleRows}\n headers={DAY_COLUMNS}\n rowKey=\"rowId\"\n variant=\"borderless\"\n enableSearch\n enableTableSort\n sortMode=\"server\"\n defaultSort={{ sortBy: 'date', sortOrder: 'asc' }}\n onParamsChange={handleParamsChange}\n searchPlaceholder=\"Buscar\"\n headerContent={\n <div className=\"flex flex-col gap-2\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Detalhes por dia\n </Text>\n <Badge\n variant=\"solid\"\n action=\"info\"\n size=\"small\"\n className=\"self-start uppercase\"\n >\n {`${allRows.length} ${allRows.length === 1 ? 'dia total' : 'dias totais'}`}\n </Badge>\n </div>\n }\n />\n {hasMore && (\n <Button\n variant=\"outline\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setExpanded((previous) => !previous)}\n >\n {expanded ? 'Mostrar menos' : 'Mostrar todos os dias'}\n {expanded ? <CaretUpIcon size={18} /> : <CaretDownIcon size={18} />}\n </Button>\n )}\n </div>\n );\n}\n\n/**\n * \"Evolução por dia\": the exams submitted each day — as bars in the color of\n * their moment, then day by day in a table. Only the days with submissions:\n * the moments run weeks apart, and the days between them would be a row of\n * empty bars.\n */\nexport function DailyEvolutionSection({\n daily,\n moments,\n}: Readonly<{\n daily: EnemMomentSectionState<EnemMomentDailyEvolution>;\n moments: EnemMomentMoment[];\n}>) {\n // Date order, whatever order the API sends: the bars read left to right and\n // \"Dia N\" counts from the first day.\n const days = useMemo(\n () =>\n [...(daily.data?.days ?? [])].sort((a, b) => a.day.localeCompare(b.day)),\n [daily.data]\n );\n\n return (\n <section className=\"flex flex-col gap-4\">\n <Text as=\"h2\" size=\"xl\" weight=\"bold\" className=\"text-text-950\">\n Evolução por dia\n </Text>\n <SectionContent\n loading={daily.loading}\n error={daily.error}\n minHeight=\"min-h-[480px]\"\n >\n <div className=\"flex flex-col gap-6 p-5 bg-background border border-border-50 rounded-xl\">\n {days.length === 0 ? (\n <div className=\"flex items-center justify-center min-h-[200px]\">\n <Text size=\"sm\" className=\"text-text-500\">\n Ainda não há provas entregues neste recorte.\n </Text>\n </div>\n ) : (\n <>\n <DailyChart days={days} moments={moments} />\n <div className=\"border-t border-border-100\" />\n <DaysTable days={days} />\n </>\n )}\n </div>\n </SectionContent>\n </section>\n );\n}\n","import { useMemo, useState } from 'react';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport Button from '../Button/Button';\nimport Select, {\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n} from '../Select/Select';\nimport {\n QuestionsBars,\n RateCell,\n type QuestionsBarsValues,\n} from '../QuestionsPerformanceCard';\nimport { StatCard } from '../StatisticsCard/StatisticsCard';\nimport {\n TableProvider,\n type ColumnConfig,\n type TableParams,\n} from '../TableProvider/TableProvider';\nimport { renderSubjectsCell } from '../../utils/renderSubjectCell';\nimport { CaretDownIcon } from '@phosphor-icons/react/dist/csr/CaretDown';\nimport { CaretUpIcon } from '@phosphor-icons/react/dist/csr/CaretUp';\nimport { SectionContent } from './SectionContent';\nimport type {\n EnemMomentQuestionsView,\n EnemMomentSectionState,\n EnemMomentSubjectRow,\n} from './types';\nimport { formatCount, formatScore, MISSING_VALUE } from './utils';\n\n/** Value of the select meaning every area. */\nconst ALL_AREAS = 'all';\n\n/** Componentes shown before \"Mostrar todos\". */\nconst COLLAPSED_SUBJECT_ROWS = 4;\n\ntype SubjectRow = {\n rowId: string;\n name: string;\n /** What `renderSubjectsCell` draws: the chip and the name. */\n subject: Array<{ id: string; name: string; color: string; icon: string }>;\n areaName: string;\n total: number;\n correct: number;\n incorrect: number;\n blank: number;\n /** `null` when nobody answered its questions. */\n rate: number | null;\n};\n\ntype SubjectSortKey =\n | 'name'\n | 'areaName'\n | 'correct'\n | 'incorrect'\n | 'blank'\n | 'rate';\n\nconst SORT_KEYS: ReadonlySet<string> = new Set<SubjectSortKey>([\n 'name',\n 'areaName',\n 'correct',\n 'incorrect',\n 'blank',\n 'rate',\n]);\n\nconst SUBJECT_COLUMNS: ColumnConfig<SubjectRow>[] = [\n {\n key: 'name',\n label: 'Componente curricular',\n sortable: true,\n className: 'min-w-[200px]',\n render: (_value, row) => renderSubjectsCell(row.subject),\n },\n {\n key: 'areaName',\n label: 'Área do conhecimento',\n sortable: true,\n className: 'min-w-[240px]',\n },\n {\n key: 'total',\n label: 'Total',\n sortable: false,\n render: (_value, row) => formatCount(row.total),\n },\n {\n key: 'correct',\n label: 'Corretas',\n sortable: true,\n render: (_value, row) => formatCount(row.correct),\n },\n {\n key: 'incorrect',\n label: 'Incorretas',\n sortable: true,\n render: (_value, row) => formatCount(row.incorrect),\n },\n {\n key: 'blank',\n label: 'Em branco',\n sortable: true,\n render: (_value, row) => formatCount(row.blank),\n },\n {\n key: 'rate',\n label: 'Taxa de acerto',\n sortable: true,\n render: (_value, row) =>\n row.rate === null ? MISSING_VALUE : <RateCell rate={row.rate} />,\n },\n];\n\n/** Sort rows by one column; names in pt-BR order. */\nfunction sortRows(\n rows: SubjectRow[],\n key: SubjectSortKey,\n order: 'asc' | 'desc'\n): SubjectRow[] {\n const direction = order === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const result =\n key === 'name' || key === 'areaName'\n ? a[key].localeCompare(b[key], 'pt-BR')\n : (a[key] ?? -1) - (b[key] ?? -1);\n return result * direction;\n });\n}\n\n/**\n * \"Desempenho por componente curricular\": searchable (by componente or area)\n * and sortable, collapsed to the first rows behind \"Mostrar todos\". Until a\n * column is sorted, the componentes keep the API's order: worst hit rate\n * first.\n */\nfunction SubjectsTable({\n subjects,\n}: Readonly<{ subjects: EnemMomentSubjectRow[] }>) {\n const [search, setSearch] = useState('');\n const [sort, setSort] = useState<{\n key: SubjectSortKey;\n order: 'asc' | 'desc';\n } | null>(null);\n const [expanded, setExpanded] = useState(false);\n\n const rows = useMemo(() => {\n const term = search.trim().toLocaleLowerCase('pt-BR');\n const all: SubjectRow[] = subjects.map((subject) => ({\n rowId: subject.subjectId,\n name: subject.subjectName,\n subject: [\n {\n id: subject.subjectId,\n name: subject.subjectName,\n color: subject.color,\n icon: subject.icon,\n },\n ],\n areaName: subject.areaKnowledgeName,\n total: subject.answered,\n correct: subject.correct,\n incorrect: subject.incorrect,\n blank: subject.blank,\n rate: subject.correctPercentage,\n }));\n const matching = term\n ? all.filter((row) =>\n `${row.name} ${row.areaName}`\n .toLocaleLowerCase('pt-BR')\n .includes(term)\n )\n : all;\n return sort ? sortRows(matching, sort.key, sort.order) : matching;\n }, [subjects, search, sort]);\n\n const visibleRows = expanded ? rows : rows.slice(0, COLLAPSED_SUBJECT_ROWS);\n const hasMore = rows.length > COLLAPSED_SUBJECT_ROWS;\n\n // Server mode: the table only reports search and sort, and they are applied\n // here to every componente before the collapse cut.\n const handleParamsChange = (params: TableParams) => {\n setSearch(params.search ?? '');\n const key = params.sortBy;\n setSort(\n key && SORT_KEYS.has(key)\n ? {\n key: key as SubjectSortKey,\n order: params.sortOrder === 'desc' ? 'desc' : 'asc',\n }\n : null\n );\n };\n\n const noun = (count: number) =>\n count === 1 ? 'componente curricular' : 'componentes curriculares';\n\n return (\n <div className=\"flex flex-col items-center gap-4\">\n <TableProvider<SubjectRow>\n tableId=\"enemMomentSubjects\"\n data={visibleRows}\n headers={SUBJECT_COLUMNS}\n rowKey=\"rowId\"\n variant=\"borderless\"\n enableSearch\n enableTableSort\n sortMode=\"server\"\n onParamsChange={handleParamsChange}\n searchPlaceholder=\"Buscar\"\n headerContent={\n <div className=\"flex flex-col gap-2\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Desempenho por componente curricular\n </Text>\n <Badge\n variant=\"solid\"\n action=\"info\"\n size=\"small\"\n className=\"self-start uppercase\"\n >\n {`${subjects.length} ${noun(subjects.length)} ${\n subjects.length === 1 ? 'total' : 'totais'\n }`}\n </Badge>\n </div>\n }\n />\n {hasMore && (\n <Button\n variant=\"outline\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setExpanded((previous) => !previous)}\n >\n {expanded\n ? 'Mostrar menos'\n : `Mostrar todos os ${rows.length} ${noun(rows.length)}`}\n {expanded ? <CaretUpIcon size={18} /> : <CaretDownIcon size={18} />}\n </Button>\n )}\n </div>\n );\n}\n\n/**\n * The card's body: the area select narrows the bars, the score and the table\n * together.\n *\n * The counts are the API's, never summed from the table: a question mapped to\n * two componentes counts in both rows but once in its area and in the totals.\n * The score is the one derived from the hit rate, for every area and for all\n * of them — the corrected score of the summary would jump against the areas'.\n */\nfunction QuestionsCard({ data }: Readonly<{ data: EnemMomentQuestionsView }>) {\n const [areaId, setAreaId] = useState(ALL_AREAS);\n\n // The select lists them in pt-BR order; the API sends worst first.\n const areas = useMemo(\n () =>\n [...data.areas].sort((a, b) =>\n a.areaKnowledgeName.localeCompare(b.areaKnowledgeName, 'pt-BR')\n ),\n [data.areas]\n );\n // An area that vanished with a new cut falls back to \"all\" on read.\n const area = areas.find((item) => item.areaKnowledgeId === areaId) ?? null;\n\n const subjects = useMemo(\n () =>\n area\n ? data.subjects.filter(\n (subject) => subject.areaKnowledgeId === area.areaKnowledgeId\n )\n : data.subjects,\n [data.subjects, area]\n );\n\n const shown = area ?? data.totals;\n const values: QuestionsBarsValues = {\n total: shown.answered,\n corretas: shown.correct,\n incorretas: shown.incorrect,\n emBranco: shown.blank,\n };\n\n return (\n <div className=\"flex flex-col gap-4 rounded-xl border border-border-50 bg-background p-5\">\n <div className=\"flex flex-row flex-wrap items-center justify-between gap-4\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Dados de questões\n </Text>\n <Select\n value={area ? area.areaKnowledgeId : ALL_AREAS}\n onValueChange={setAreaId}\n className=\"w-[380px] max-w-full\"\n >\n <SelectTrigger className=\"w-full\" aria-label=\"Área do conhecimento\">\n <SelectValue placeholder=\"Todas as áreas do conhecimento\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_AREAS}>\n Todas as áreas do conhecimento\n </SelectItem>\n {areas.map((item) => (\n <SelectItem\n key={item.areaKnowledgeId}\n value={item.areaKnowledgeId}\n >\n {item.areaKnowledgeName}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <QuestionsBars\n values={values}\n aside={\n <StatCard\n item={{\n label: 'Nota média',\n value:\n shown.averageScore === null\n ? MISSING_VALUE\n : formatScore(shown.averageScore),\n variant: 'total',\n }}\n />\n }\n />\n\n <div aria-hidden=\"true\" className=\"h-px w-full bg-border-200\" />\n\n <SubjectsTable subjects={subjects} />\n </div>\n );\n}\n\n/**\n * \"Desempenho por área de conhecimento\": how the answers split — overall or\n * for one area — and how each componente curricular did.\n */\nexport function KnowledgeAreaSection({\n questions,\n}: Readonly<{\n questions: EnemMomentSectionState<EnemMomentQuestionsView>;\n}>) {\n return (\n <section className=\"flex flex-col gap-4\">\n <Text as=\"h2\" size=\"xl\" weight=\"bold\" className=\"text-text-950\">\n Desempenho por área de conhecimento\n </Text>\n <SectionContent\n loading={questions.loading}\n error={questions.error}\n minHeight=\"min-h-[560px]\"\n >\n {questions.data && <QuestionsCard data={questions.data} />}\n </SectionContent>\n </section>\n );\n}\n","import type { ReactNode } from 'react';\nimport { MedalIcon } from '@phosphor-icons/react/dist/csr/Medal';\nimport { SealWarningIcon } from '@phosphor-icons/react/dist/csr/SealWarning';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport {\n RankingCard,\n type StudentRankingItem,\n type StudentRankingVariant,\n} from '../StudentRanking/StudentRanking';\nimport { SectionContent } from './SectionContent';\nimport type { EnemMomentSectionState, EnemMomentStudentRow } from './types';\nimport { formatScore } from './utils';\n\n/** Students each ranking card lists. */\nexport const RANKING_ROWS = 3;\n\n/** Both ranking cards, when nobody of the cut has a score. */\nexport const EMPTY_RANKING_TEXT =\n 'Nenhum estudante finalizou o simulado neste recorte.';\n\n/** A student of a ranking, with their position in it. */\nexport type EnemMomentRankedStudent = EnemMomentStudentRow & {\n position: number;\n averageScore: number;\n};\n\n/** \"Nota 9,0\" where the lib's row prints a percentage, in the card's color. */\nexport function ScoreBadge({\n score,\n variant,\n}: Readonly<{ score: number; variant: StudentRankingVariant }>) {\n return (\n <Text\n size=\"xs\"\n weight=\"medium\"\n className={cn(\n 'shrink-0 flex items-center h-5.5 px-2 rounded bg-background',\n variant === 'highlight' ? 'text-success-700' : 'text-error-700'\n )}\n >\n {`Nota ${formatScore(score)}`}\n </Text>\n );\n}\n\n/**\n * A ranking card of the report — the lib's, with its loading and error, whose\n * row click hands back the student of that row.\n */\nexport function EnemMomentRankingCard({\n title,\n variant,\n ranking,\n toItem,\n headerIcon,\n emptyText = EMPTY_RANKING_TEXT,\n footer,\n onStudentClick,\n}: Readonly<{\n title: string;\n variant: StudentRankingVariant;\n ranking: EnemMomentSectionState<EnemMomentRankedStudent[]>;\n toItem: (student: EnemMomentRankedStudent) => StudentRankingItem;\n headerIcon: ReactNode;\n emptyText?: string;\n footer?: ReactNode;\n /** Opens the student's modal. Without it the rows are not clickable. */\n onStudentClick?: (student: EnemMomentRankedStudent) => void;\n}>) {\n const top = (ranking.data ?? []).slice(0, RANKING_ROWS);\n\n // The card hands back the row it drew, so the student comes from the id it\n // carries — the enrolment, which is what the modal is fetched by.\n const handleClick = onStudentClick\n ? (item: StudentRankingItem) => {\n const student = top.find(\n (candidate) => candidate.userInstitutionId === item.id\n );\n if (student) onStudentClick(student);\n }\n : undefined;\n\n return (\n <SectionContent\n loading={ranking.loading}\n error={ranking.error}\n minHeight=\"min-h-[254px]\"\n >\n <RankingCard\n title={title}\n variant={variant}\n students={top.map(toItem)}\n onStudentClick={handleClick}\n headerIcon={headerIcon}\n emptyText={emptyText}\n footer={footer}\n />\n </SectionContent>\n );\n}\n\n/**\n * A student of the unit's rankings: the name alone — the school is the\n * report's — and the score in the card's color.\n */\nconst toUnitRankingItem =\n (variant: StudentRankingVariant) =>\n (student: EnemMomentRankedStudent): StudentRankingItem => ({\n id: student.userInstitutionId,\n position: student.position,\n name: student.studentName,\n percentage: student.hitRate ?? 0,\n badge: <ScoreBadge score={student.averageScore} variant={variant} />,\n });\n\nconst toHighlightItem = toUnitRankingItem('highlight');\nconst toAttentionItem = toUnitRankingItem('attention');\n\n/**\n * \"Estudantes em destaque\" and \"Estudantes com maior dificuldade\" side by\n * side: the top and the bottom three scores of the unit's cut.\n */\nexport function UnitStudentRankings({\n highlights,\n struggling,\n onStudentClick,\n}: Readonly<{\n highlights: EnemMomentSectionState<EnemMomentRankedStudent[]>;\n struggling: EnemMomentSectionState<EnemMomentRankedStudent[]>;\n /** Opens the student's modal. Without it the rows are not clickable. */\n onStudentClick?: (student: EnemMomentRankedStudent) => void;\n}>) {\n return (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <EnemMomentRankingCard\n title=\"Estudantes em destaque\"\n variant=\"highlight\"\n ranking={highlights}\n toItem={toHighlightItem}\n onStudentClick={onStudentClick}\n headerIcon={\n <MedalIcon size={14} className=\"text-text-950\" aria-hidden=\"true\" />\n }\n />\n <EnemMomentRankingCard\n title=\"Estudantes com maior dificuldade\"\n variant=\"attention\"\n ranking={struggling}\n toItem={toAttentionItem}\n onStudentClick={onStudentClick}\n headerIcon={\n <SealWarningIcon size={14} className=\"text-text\" aria-hidden=\"true\" />\n }\n />\n </div>\n );\n}\n","import { useMemo } from 'react';\nimport { ExamIcon } from '@phosphor-icons/react/dist/csr/Exam';\nimport { NavigationArrowIcon } from '@phosphor-icons/react/dist/csr/NavigationArrow';\nimport { ProhibitIcon } from '@phosphor-icons/react/dist/csr/Prohibit';\nimport { StudentIcon } from '@phosphor-icons/react/dist/csr/Student';\nimport {\n TimeReport,\n type TimeCardData,\n type TimeReportTab,\n} from '../TimeReport/TimeReport';\nimport { SectionContent } from './SectionContent';\nimport type { EnemMomentSectionState, EnemMomentSummary } from './types';\nimport { formatCount, formatPercentage } from './utils';\n\nconst plural = (count: number, singular: string, pluralForm: string) =>\n `${formatCount(count)} ${count === 1 ? singular : pluralForm}`;\n\n/**\n * The four cards of the unit report (Gestor de Unidade), all counting\n * students of its school:\n *\n * Card 1 — Total de estudantes : totalStudents | \"1 escola • 134 turmas\"\n * Card 2 — Realizaram o simulado : participatingStudents | \"de N estudantes\"\n * Card 3 — % de realização : studentParticipationPercentage | \"N ainda não realizaram\"\n * Card 4 — Estudantes sem realizar o simulado : studentsWithoutParticipation | \"não realizaram\"\n */\nexport function buildEnemMomentUnitSummaryCards(\n summary: EnemMomentSummary\n): TimeCardData[] {\n const where = `${plural(summary.totalSchools, 'escola', 'escolas')} • ${plural(summary.totalClasses, 'turma', 'turmas')}`;\n\n return [\n {\n id: 'totalStudents',\n label: 'TOTAL DE ESTUDANTES',\n value: formatCount(summary.totalStudents),\n icon: <StudentIcon />,\n footer: where,\n },\n {\n id: 'participatingStudents',\n label: 'REALIZARAM O SIMULADO',\n value: formatCount(summary.participatingStudents),\n icon: <ExamIcon />,\n footer: `de ${plural(summary.totalStudents, 'estudante', 'estudantes')}`,\n },\n {\n id: 'studentParticipationPercentage',\n label: '% DE REALIZAÇÃO',\n value: formatPercentage(summary.studentParticipationPercentage),\n icon: <NavigationArrowIcon />,\n footer: `${formatCount(summary.studentsWithoutParticipation)} ainda não realizaram`,\n },\n {\n id: 'studentsWithoutParticipation',\n label: 'ESTUDANTES SEM REALIZAR O SIMULADO',\n value: formatCount(summary.studentsWithoutParticipation),\n icon: <ProhibitIcon />,\n footer: 'não realizaram',\n },\n ];\n}\n\n/** The cards block has no profile tabs: a single, stripless one. */\nconst SUMMARY_TAB_VALUE = 'enem-moment';\n\n/**\n * The row of four cards at the top of the report, on the lib's TimeReport —\n * the network's cards or the unit's (`buildCards`).\n */\nexport function EnemMomentSummaryCards({\n summary,\n buildCards,\n}: Readonly<{\n summary: EnemMomentSectionState<EnemMomentSummary>;\n buildCards: (summary: EnemMomentSummary) => TimeCardData[];\n}>) {\n const tabs = useMemo<TimeReportTab[]>(\n () => [\n {\n value: SUMMARY_TAB_VALUE,\n label: '',\n cards: summary.data ? buildCards(summary.data) : [],\n },\n ],\n [summary.data, buildCards]\n );\n\n return (\n <SectionContent\n loading={summary.loading}\n error={summary.error}\n minHeight=\"min-h-[140px]\"\n >\n <TimeReport tabs={tabs} activeTab={SUMMARY_TAB_VALUE} />\n </SectionContent>\n );\n}\n","import { ReportTabs } from '../shared/ReportTabs';\n\n/** One tab of the report: \"Geral\" or one exam. */\nexport interface EnemMomentTab {\n value: string;\n label: string;\n}\n\n/**\n * The \"Geral\" / \"Momento N\" strip of the Momento ENEM screens — the lib's\n * report tabs, the look of the profile tabs of the other reports. Only the\n * active tab prints: the others are navigation, not content.\n *\n * Not drawn with \"Geral\" plus a single exam: two tabs showing the same numbers.\n */\nexport function MomentTabs({\n tabs,\n activeTab,\n onTabChange,\n}: Readonly<{\n tabs: EnemMomentTab[];\n activeTab: string;\n onTabChange: (value: string) => void;\n}>) {\n if (tabs.length <= 2) return null;\n\n return (\n <ReportTabs\n tabs={tabs}\n value={activeTab}\n onValueChange={onTabChange}\n printActiveOnly\n />\n );\n}\n","import { ClockIcon } from '@phosphor-icons/react/dist/csr/Clock';\nimport Select, {\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n} from '../Select/Select';\nimport { MultiCheckSelect } from '../MultiCheckSelect/MultiCheckSelect';\nimport { EDUCATION_STAGE_OPTIONS, EXAM_DURATION_OPTIONS } from './constants';\nimport type { EnemMomentEducationStage } from './types';\n\nconst formatSelectedDurations = (count: number) =>\n count === 1 ? '1 tempo selecionado' : `${count} tempos selecionados`;\n\n/**\n * The picks of the Momento ENEM screens: \"Tempo de prova\" and the teaching\n * stage of the classes. They sit in the page's header, in place of the period\n * and filters of the other reports.\n */\nexport function EnemMomentHeaderFilters({\n durations,\n onDurationsChange,\n educationStage,\n onEducationStageChange,\n}: Readonly<{\n /** \"Tempo de prova\" picks, in minutes; empty is every time. */\n durations: number[];\n onDurationsChange: (durations: number[]) => void;\n educationStage: EnemMomentEducationStage;\n onEducationStageChange: (stage: EnemMomentEducationStage) => void;\n}>) {\n return (\n <>\n <MultiCheckSelect\n options={EXAM_DURATION_OPTIONS}\n values={durations}\n onValuesChange={onDurationsChange}\n placeholder=\"Tempo de prova\"\n allLabel=\"Todos os tempos\"\n formatSelected={formatSelectedDurations}\n icon={<ClockIcon size={18} aria-hidden=\"true\" />}\n aria-label=\"Tempo de prova\"\n />\n\n <Select\n value={educationStage}\n onValueChange={(value) =>\n onEducationStageChange(value as EnemMomentEducationStage)\n }\n >\n <SelectTrigger className=\"w-auto min-w-45 gap-2\" aria-label=\"Série\">\n <SelectValue placeholder=\"Série\" />\n </SelectTrigger>\n <SelectContent>\n {EDUCATION_STAGE_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </>\n );\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport Text from '../Text/Text';\nimport { UserIcon } from '../UserIcon/UserIcon';\nimport { cn } from '../../utils/utils';\nimport {\n TableProvider,\n type ColumnConfig,\n type TableComponents,\n type TableParams,\n} from '../TableProvider/TableProvider';\nimport { RateCell } from '../QuestionsPerformanceCard';\nimport { SectionContent } from './SectionContent';\nimport { StudentsTableDownload } from './StudentsDownload';\nimport {\n ENEM_MOMENT_PERFORMANCE_LABELS,\n ENEM_MOMENT_PERFORMANCE_ORDER,\n EnemMomentPerformanceBadge,\n ParticipationBadge,\n type EnemMomentStudentParticipation,\n} from './PerformanceBadge';\nimport type {\n EnemMomentFilterOptions,\n EnemMomentPagination,\n EnemMomentParticipationFilter,\n EnemMomentPerformance,\n EnemMomentStudentRow,\n EnemMomentStudentsExport,\n EnemMomentStudentsOrderBy,\n EnemMomentStudentsQuery,\n} from './types';\nimport { classOptionLabel, listFilter, pickedValue } from './tableFilters';\nimport { formatClock, formatCount, formatScore, MISSING_VALUE } from './utils';\n\nexport function participationOf(\n row: EnemMomentStudentRow\n): EnemMomentStudentParticipation {\n if (row.participatedExams === 0) return 'NOT_PARTICIPATED';\n return row.partialParticipation ? 'PARTIAL' : 'PARTICIPATED';\n}\n\n/**\n * The last column: whether the student took the exams (a school's page) or\n * the tier they scored in (the unit report), each with its filter.\n */\nexport type StudentStatusColumn = 'participation' | 'performance';\n\nconst PARTICIPATION_COLUMN: ColumnConfig<EnemMomentStudentRow> = {\n key: 'participatedExams',\n label: 'Participação',\n filter: {\n paramKey: 'participation',\n allLabel: 'Todos',\n options: [\n {\n value: 'PARTICIPATED',\n label: <ParticipationBadge participation=\"PARTICIPATED\" />,\n searchText: 'Participou',\n },\n {\n value: 'NOT_PARTICIPATED',\n label: <ParticipationBadge participation=\"NOT_PARTICIPATED\" />,\n searchText: 'Não participou',\n },\n ],\n },\n render: (_value, row) => (\n <ParticipationBadge participation={participationOf(row)} />\n ),\n};\n\nconst PERFORMANCE_COLUMN: ColumnConfig<EnemMomentStudentRow> = {\n key: 'performance',\n label: 'Desempenho',\n filter: {\n paramKey: 'performances',\n multiple: true,\n allLabel: 'Todos',\n options: ENEM_MOMENT_PERFORMANCE_ORDER.map((performance) => ({\n value: performance,\n label: <EnemMomentPerformanceBadge performance={performance} />,\n searchText: ENEM_MOMENT_PERFORMANCE_LABELS[performance],\n })),\n },\n // Started, with no score yet: in no tier — the same \"—\" as the Nota column.\n render: (_value, row) =>\n row.performance === null ? (\n MISSING_VALUE\n ) : (\n <EnemMomentPerformanceBadge performance={row.performance} />\n ),\n};\n\n/** Whether the student took any exam of the cut — only then is there a modal. */\nexport const tookAnyExam = (row: EnemMomentStudentRow) =>\n row.participatedExams > 0;\n\n/** An answer count — \"—\" for a student who took nothing, not a zero. */\nconst count = (row: EnemMomentStudentRow, value: number) =>\n row.participatedExams === 0 ? MISSING_VALUE : formatCount(value);\n\n/**\n * The columns the API orders by, by the table's key — the name is `name`\n * there. Every column but Turma and the last one.\n */\nconst ORDER_BY: Readonly<Record<string, EnemMomentStudentsOrderBy>> = {\n studentName: 'name',\n totalElapsedSeconds: 'totalElapsedSeconds',\n correct: 'correct',\n incorrect: 'incorrect',\n blank: 'blank',\n hitRate: 'hitRate',\n averageScore: 'averageScore',\n};\n\nconst PARTICIPATION_FILTERS: ReadonlySet<string> =\n new Set<EnemMomentParticipationFilter>(['PARTICIPATED', 'NOT_PARTICIPATED']);\n\n/**\n * Columns of the table. The Turma filter offers the school's classes; until\n * they arrive there is nothing to offer, and it waits.\n */\nexport function createStudentColumns(\n classes: EnemMomentFilterOptions['classes'],\n statusColumn: StudentStatusColumn = 'participation'\n): ColumnConfig<EnemMomentStudentRow>[] {\n return [\n {\n key: 'studentName',\n label: 'Nome',\n className: 'min-w-[220px] max-w-[320px]',\n render: (_value, row) => (\n <div className=\"flex items-center gap-2 min-w-0\">\n <UserIcon size={24} className=\"shrink-0\" />\n <Text size=\"sm\" className=\"text-text-950 truncate\">\n {row.studentName}\n </Text>\n </div>\n ),\n },\n {\n key: 'className',\n label: 'Turma',\n filter: listFilter(\n 'classIds',\n 'Todas as turmas',\n 'Buscar turma...',\n classes.map((option) => ({\n value: option.classId,\n label: classOptionLabel(option),\n }))\n ),\n render: (_value, row) => row.className ?? MISSING_VALUE,\n },\n {\n key: 'totalElapsedSeconds',\n label: 'Tempo',\n render: (_value, row) => formatClock(row.totalElapsedSeconds),\n },\n {\n key: 'correct',\n label: 'Corretas',\n render: (_value, row) => count(row, row.correct),\n },\n {\n key: 'incorrect',\n label: 'Incorretas',\n render: (_value, row) => count(row, row.incorrect),\n },\n {\n key: 'blank',\n label: 'Em branco',\n render: (_value, row) => count(row, row.blank),\n },\n {\n key: 'hitRate',\n label: 'Taxa de acerto',\n className: 'min-w-[160px]',\n render: (_value, row) =>\n row.hitRate === null ? MISSING_VALUE : <RateCell rate={row.hitRate} />,\n },\n {\n key: 'averageScore',\n label: 'Nota',\n render: (_value, row) =>\n row.averageScore === null\n ? MISSING_VALUE\n : formatScore(row.averageScore),\n },\n statusColumn === 'performance' ? PERFORMANCE_COLUMN : PARTICIPATION_COLUMN,\n ];\n}\n\n/** The tiers picked in the Desempenho filter — none is every one. */\nconst pickedPerformances = (value: unknown) =>\n Array.isArray(value) && value.length > 0\n ? (value as EnemMomentPerformance[])\n : undefined;\n\n/**\n * What the table asks the API for: its page, search, order and filters — the\n * app adds the cut and fetches.\n */\nexport type EnemMomentStudentsTableQuery = Pick<\n EnemMomentStudentsQuery,\n | 'page'\n | 'limit'\n | 'search'\n | 'classIds'\n | 'participation'\n | 'performances'\n | 'orderBy'\n | 'order'\n>;\n\n/**\n * What the table is filtering by, without the page it is on: the search, the\n * column filters and the order. The PDF of \"Baixar tabela\" asks for every\n * page under these.\n */\nexport type EnemMomentStudentsTableFilters = Omit<\n EnemMomentStudentsTableQuery,\n 'page' | 'limit'\n>;\n\n/** The filters of a table query — the query without its page. */\nexport function toStudentsTableFilters(\n query: EnemMomentStudentsTableQuery\n): EnemMomentStudentsTableFilters {\n return {\n search: query.search,\n classIds: query.classIds,\n participation: query.participation,\n performances: query.performances,\n orderBy: query.orderBy,\n order: query.order,\n };\n}\n\n/** The table's params as the students list reads them. */\nexport function toStudentsTableQuery(\n params: TableParams\n): EnemMomentStudentsTableQuery {\n const orderBy = params.sortBy ? ORDER_BY[params.sortBy] : undefined;\n const classId = pickedValue(params.classIds);\n const participation = pickedValue(params.participation);\n return {\n page: params.page,\n limit: params.limit,\n search: params.search || undefined,\n classIds: classId ? [classId] : undefined,\n participation:\n participation && PARTICIPATION_FILTERS.has(participation)\n ? (participation as EnemMomentParticipationFilter)\n : undefined,\n performances: pickedPerformances(params.performances),\n orderBy,\n order: orderBy ? params.sortOrder : undefined,\n };\n}\n\n/**\n * Whether the table has already finished one request. A remount (the app keys\n * the table on the cut) starts it over: the rows in memory are then another\n * cut's.\n */\nfunction useHasSettled(loading: boolean): boolean {\n const wasLoadingRef = useRef(false);\n const [hasSettled, setHasSettled] = useState(false);\n\n useEffect(() => {\n if (wasLoadingRef.current && !loading) setHasSettled(true);\n wasLoadingRef.current = loading;\n }, [loading]);\n\n return hasSettled;\n}\n\n/**\n * \"Desempenho por estudante\": every student the exams were sent to — how they\n * did, or that they did not take it — paged, searched, ordered and filtered\n * by the API. A student who took the exam opens their modal; one who took\n * nothing has nothing to open.\n *\n * The app fetches: the table reports what it asks for through `onQueryChange`\n * and draws the rows it is handed. Key it on the cut, so a new tab or pick\n * starts it over from page 1. While a page loads the previous rows stay on\n * screen, dimmed: swapping them for a skeleton would collapse the page and\n * throw the scroll back to the top.\n *\n * With `loadExport` the header offers \"Baixar tabela\", left of the search:\n * the spreadsheet carries the whole list of the caller's students, whatever\n * the table is showing. With `loadFilteredStudents` too, the PDF prints only\n * the students the table lists under its current filters — every page, in\n * its order.\n */\nexport function StudentsTableSection({\n rows,\n pagination,\n loading,\n error,\n classes,\n onQueryChange,\n onStudentClick,\n statusColumn = 'participation',\n tableId = 'enemMomentStudents',\n loadExport,\n loadFilteredStudents,\n}: Readonly<{\n rows: EnemMomentStudentRow[];\n pagination: EnemMomentPagination | null;\n loading: boolean;\n error: string | null;\n /** The Turma filter's options; empty while they load. */\n classes: EnemMomentFilterOptions['classes'];\n onQueryChange: (query: EnemMomentStudentsTableQuery) => void;\n onStudentClick?: (student: EnemMomentStudentRow) => void;\n /** The last column — Participação unless told otherwise. */\n statusColumn?: StudentStatusColumn;\n /** Namespaces the table's params in the URL. */\n tableId?: string;\n /** Fetches the whole list for \"Baixar tabela\"; without it, no button. */\n loadExport?: () => Promise<EnemMomentStudentsExport>;\n /**\n * Every student the table lists under these filters, all pages, in its\n * order — the app adds the cut, as for `onQueryChange`. Narrows the PDF;\n * without it the PDF carries the whole list, like the spreadsheet.\n */\n loadFilteredStudents?: (\n filters: EnemMomentStudentsTableFilters\n ) => Promise<EnemMomentStudentRow[]>;\n}>) {\n const columns = useMemo(\n () => createStudentColumns(classes, statusColumn),\n [classes, statusColumn]\n );\n\n const hasSettled = useHasSettled(loading);\n const showSkeleton = loading && (!hasSettled || rows.length === 0);\n const isRefreshing = loading && !showSkeleton;\n\n const handleRowClick = useCallback(\n (row: EnemMomentStudentRow) => onStudentClick?.(row),\n [onStudentClick]\n );\n\n // Read when the PDF is asked for, not rendered: a ref, so the download\n // prints what the table is filtering at that moment.\n const filtersRef = useRef<EnemMomentStudentsTableFilters>({});\n\n const handleParamsChange = useCallback(\n (params: TableParams) => {\n const query = toStudentsTableQuery(params);\n filtersRef.current = toStudentsTableFilters(query);\n onQueryChange(query);\n },\n [onQueryChange]\n );\n\n const loadPdfStudents = useCallback(\n () => loadFilteredStudents?.(filtersRef.current) ?? Promise.resolve([]),\n [loadFilteredStudents]\n );\n\n const title = (\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Desempenho por estudante\n </Text>\n );\n\n // The provider's own header has no room for an action by the search, so\n // the download brings its own row: title left, button and search right.\n const renderHeader = (components: TableComponents): ReactNode =>\n loadExport ? (\n <div className=\"flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-4\">\n {title}\n <div className=\"flex flex-wrap items-center justify-end gap-4\">\n <StudentsTableDownload\n loadExport={loadExport}\n loadPdfStudents={loadFilteredStudents ? loadPdfStudents : undefined}\n />\n <div className=\"flex-1 lg:flex-none lg:w-[488px] print:hidden\">\n {components.search}\n </div>\n </div>\n </div>\n ) : (\n components.controls\n );\n\n const renderLayout = (components: TableComponents): ReactNode => (\n <div className=\"bg-background border border-border-50 rounded-xl p-6 space-y-4\">\n {renderHeader(components)}\n <div\n aria-busy={isRefreshing}\n className={cn(\n 'transition-opacity',\n isRefreshing && 'opacity-50 pointer-events-none'\n )}\n >\n {components.table}\n </div>\n {components.pagination && (\n <div data-print-hide>{components.pagination}</div>\n )}\n </div>\n );\n\n return (\n <SectionContent loading={false} error={error} minHeight=\"min-h-[200px]\">\n <TableProvider<EnemMomentStudentRow>\n tableId={tableId}\n data={rows}\n headers={columns}\n loading={showSkeleton}\n variant=\"borderless\"\n enableSearch\n enableTableSort\n sortMode=\"server\"\n sortableColumns={Object.keys(ORDER_BY)}\n defaultSort={{ sortBy: 'studentName', sortOrder: 'asc' }}\n enablePagination\n enableRowClick={Boolean(onStudentClick)}\n onRowClick={handleRowClick}\n rowKey=\"userInstitutionId\"\n searchPlaceholder=\"Buscar\"\n searchContainerClassName=\"print:hidden\"\n headerContent={title}\n paginationConfig={{\n itemLabel: 'estudantes',\n itemsPerPageOptions: [10, 20, 50, 100],\n defaultItemsPerPage: 10,\n totalItems: pagination?.total ?? 0,\n totalPages: pagination?.totalPages ?? 0,\n }}\n onParamsChange={handleParamsChange}\n // A student who took nothing has nothing to open.\n isRowClickable={tookAnyExam}\n emptyState={{\n title: 'Nenhum estudante encontrado',\n description:\n 'Não há estudantes para o recorte e os filtros aplicados. Ajuste a busca ou os filtros.',\n }}\n >\n {renderLayout}\n </TableProvider>\n </SectionContent>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useReactToPrint } from 'react-to-print';\nimport { DownloadSimpleIcon } from '@phosphor-icons/react/dist/csr/DownloadSimple';\nimport Button from '../Button/Button';\nimport DownloadModal from '../DownloadModal/DownloadModal';\nimport { downloadExcel, type SheetConfig } from '../../utils/exportExcel';\nimport {\n formatDateForFileName,\n formatTimestamp,\n} from '../../utils/exportFormat';\nimport type {\n EnemMomentExamStatus,\n EnemMomentStudentRow,\n EnemMomentStudentsExport,\n} from './types';\nimport { MISSING_VALUE } from './utils';\n\n/** How a status reads in the download. */\nexport const ENEM_MOMENT_EXAM_STATUS_LABELS: Readonly<\n Record<EnemMomentExamStatus, string>\n> = {\n DONE: 'Fez',\n PARTIAL: 'Parcial',\n NOT_DONE: 'Não fez',\n};\n\n/** What each status means, printed under the PDF's table. */\nconst STATUS_LEGEND = [\n 'Fez: respondeu todas as questões',\n 'Parcial: deixou questões em branco ou não entregou a prova',\n 'Não fez: não abriu a prova',\n].join(' · ');\n\nconst DOWNLOAD_ERROR = 'Não foi possível baixar a tabela. Tente novamente.';\n\nconst NOTHING_LISTED = 'Nenhum estudante corresponde aos filtros da tabela.';\n\nconst NOBODY = 'Não há estudantes para baixar.';\n\n/** A student the table lists — all the PDF needs to pick them out. */\ntype ListedStudent = Pick<EnemMomentStudentRow, 'userInstitutionId'>;\n\n/**\n * The columns of the download: the student, then one status per exam — as\n * many as the flag declares, named by their order, like the tabs.\n */\nexport function studentsExportHeaders(\n data: EnemMomentStudentsExport\n): string[] {\n return [\n 'Nome',\n 'E-mail',\n 'Turma',\n ...data.exams.map((_exam, index) => `Status Momento ${index + 1}`),\n ];\n}\n\n/**\n * One row per student, the statuses in the order of `exams`. An exam the\n * student carries no status for reads \"Não fez\": nothing of theirs was found.\n */\nexport function studentsExportRows(data: EnemMomentStudentsExport): string[][] {\n return data.students.map((student) => {\n const statusByExam = new Map(\n student.moments.map((moment) => [moment.examId, moment.status])\n );\n return [\n student.studentName,\n student.email,\n student.className ?? MISSING_VALUE,\n ...data.exams.map(\n (exam) =>\n ENEM_MOMENT_EXAM_STATUS_LABELS[\n statusByExam.get(exam.examId) ?? 'NOT_DONE'\n ]\n ),\n ];\n });\n}\n\n/**\n * The whole list cut down to the students the table lists, in the table's\n * order. A listed student missing from the list is left out: there is no\n * e-mail or status to print for them.\n */\nexport function narrowStudentsExport(\n data: EnemMomentStudentsExport,\n listed: readonly ListedStudent[]\n): EnemMomentStudentsExport {\n const byId = new Map(\n data.students.map((student) => [student.userInstitutionId, student])\n );\n return {\n exams: data.exams,\n students: listed.flatMap((row) => byId.get(row.userInstitutionId) ?? []),\n };\n}\n\n/** The download as a spreadsheet sheet. */\nexport function studentsExportSheet(\n data: EnemMomentStudentsExport\n): SheetConfig {\n return {\n name: 'Estudantes',\n headers: studentsExportHeaders(data),\n rows: studentsExportRows(data),\n };\n}\n\n/** The file's name, stamped with the day it was downloaded. */\nexport const studentsExportFileName = (date: Date = new Date()) =>\n `relatorio-simulados-momento-enem-tabela-estudantes-${formatDateForFileName(date)}`;\n\nconst PAGE_STYLE = `\n @page { size: A4 portrait; margin: 12mm; }\n @media print {\n * {\n -webkit-print-color-adjust: exact !important;\n print-color-adjust: exact !important;\n }\n }\n`;\n\n/** The list the PDF prints: the same columns as the spreadsheet. */\nfunction StudentsExportPrintable({\n data,\n filtered,\n generatedAt,\n}: Readonly<{\n data: EnemMomentStudentsExport;\n /** Whether the list is the table's, under its filters. */\n filtered: boolean;\n generatedAt: Date;\n}>) {\n const headers = studentsExportHeaders(data);\n const rows = studentsExportRows(data);\n\n return (\n <div className=\"p-2 text-text-950 font-sans\">\n <h1 className=\"text-lg font-bold\">Simulados Momento ENEM</h1>\n <p className=\"text-sm text-text-700\">\n Desempenho por estudante\n {filtered && ' · Conforme os filtros da tabela'} · Gerado em{' '}\n {formatTimestamp(generatedAt)} · {rows.length} estudantes\n </p>\n <table className=\"mt-4 w-full border-collapse text-xs\">\n <thead>\n <tr>\n {headers.map((header) => (\n <th\n key={header}\n className=\"border border-border-200 bg-background-50 px-2 py-1 text-left font-bold\"\n >\n {header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, rowIndex) => (\n <tr\n key={data.students[rowIndex].userInstitutionId}\n className=\"break-inside-avoid\"\n >\n {row.map((cell, cellIndex) => (\n <td\n key={headers[cellIndex]}\n className=\"border border-border-200 px-2 py-1\"\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n <p className=\"mt-3 text-xs text-text-700\">{STATUS_LEGEND}</p>\n </div>\n );\n}\n\n/**\n * \"Baixar tabela\" of the students table: the students with their status in\n * each exam, as a spreadsheet or a PDF.\n *\n * The spreadsheet carries every student in the caller's scope, whatever the\n * table is showing — it can be filtered in the spreadsheet itself. The PDF,\n * given `loadPdfStudents`, carries only the students the table lists under\n * its filters, in its order; without it, the whole list too.\n *\n * The app fetches; both formats ask when the user confirms, never before, and\n * the modal holds its skeleton while the lists arrive. The PDF is the\n * browser's print of a list rendered off screen for it.\n */\nexport function StudentsTableDownload({\n loadExport,\n loadPdfStudents,\n}: Readonly<{\n /** `GET /enem-moment-report/students/export`, through the app's client. */\n loadExport: () => Promise<EnemMomentStudentsExport>;\n /** Every student the table lists under its filters, all pages, in order. */\n loadPdfStudents?: () => Promise<ListedStudent[]>;\n}>) {\n const [isOpen, setIsOpen] = useState(false);\n const [isDownloading, setIsDownloading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [printData, setPrintData] = useState<{\n data: EnemMomentStudentsExport;\n filtered: boolean;\n generatedAt: Date;\n } | null>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const printedRef = useRef<object | null>(null);\n\n const clearPrintData = useCallback(() => setPrintData(null), []);\n\n const print = useReactToPrint({\n contentRef,\n documentTitle: studentsExportFileName(),\n pageStyle: PAGE_STYLE,\n onAfterPrint: clearPrintData,\n });\n\n // The list is in the DOM only after the render that received it. Once per\n // list: `print` changes identity with its options, and the modal closing\n // re-renders this component while the list is still mounted.\n useEffect(() => {\n if (printData && printedRef.current !== printData) {\n printedRef.current = printData;\n print();\n }\n }, [printData, print]);\n\n const open = useCallback(() => {\n setError(null);\n setIsOpen(true);\n }, []);\n\n const close = useCallback(() => setIsOpen(false), []);\n\n const run = useCallback(async (task: () => Promise<void>) => {\n setIsDownloading(true);\n setError(null);\n try {\n await task();\n } catch {\n setError(DOWNLOAD_ERROR);\n } finally {\n setIsDownloading(false);\n }\n }, []);\n\n const downloadExcelFile = useCallback(\n () =>\n run(async () => {\n const data = await loadExport();\n downloadExcel(studentsExportFileName(), [studentsExportSheet(data)]);\n }),\n [run, loadExport]\n );\n\n const downloadPdf = useCallback(\n () =>\n run(async () => {\n const [data, listed] = await Promise.all([\n loadExport(),\n loadPdfStudents?.() ?? null,\n ]);\n const printed = listed ? narrowStudentsExport(data, listed) : data;\n // Nothing to print: say so in the modal rather than open an empty page.\n if (printed.students.length === 0) {\n setError(listed ? NOTHING_LISTED : NOBODY);\n return;\n }\n setPrintData({\n data: printed,\n filtered: listed !== null,\n generatedAt: new Date(),\n });\n }),\n [run, loadExport, loadPdfStudents]\n );\n\n return (\n <>\n <div data-print-hide className=\"print:hidden\">\n <Button\n variant=\"outline\"\n action=\"primary\"\n size=\"small\"\n iconLeft={<DownloadSimpleIcon size={16} />}\n onClick={open}\n data-testid=\"enem-moment-students-download-btn\"\n >\n Baixar tabela\n </Button>\n </div>\n\n <DownloadModal\n isOpen={isOpen}\n onClose={close}\n title=\"Como deseja baixar a tabela?\"\n isDownloading={isDownloading}\n error={error}\n onDownloadPdf={downloadPdf}\n onDownloadExcel={downloadExcelFile}\n asyncPdf\n />\n\n {printData && (\n <div className=\"hidden\">\n <div ref={contentRef} data-testid=\"enem-moment-students-printable\">\n <StudentsExportPrintable\n data={printData.data}\n filtered={printData.filtered}\n generatedAt={printData.generatedAt}\n />\n </div>\n </div>\n )}\n </>\n );\n}\n","import type { ColumnFilterConfig } from '../TableProvider/TableProvider';\nimport type { EnemMomentFilterOptions } from './types';\n\n/**\n * A searchable, single-choice header filter — the Município and Turma filters\n * of the Momento ENEM tables.\n */\nexport const listFilter = (\n paramKey: string,\n allLabel: string,\n searchPlaceholder: string,\n options: ReadonlyArray<{ value: string; label: string }>\n): ColumnFilterConfig => ({\n paramKey,\n allLabel,\n searchable: true,\n searchPlaceholder,\n options: options.map((option) => ({\n value: option.value,\n label: option.label,\n searchText: option.label,\n })),\n});\n\n/** A class as the Turma filter names it: \"A (3ª série)\", \"Sem turma\". */\nexport const classOptionLabel = (\n option: EnemMomentFilterOptions['classes'][number]\n) => {\n const name = option.className ?? 'Sem turma';\n return option.schoolYearName ? `${name} (${option.schoolYearName})` : name;\n};\n\n/**\n * The single value a filter reports in the table params: a string, or nothing\n * for \"all\".\n */\nexport const pickedValue = (value: unknown): string | undefined =>\n typeof value === 'string' && value !== '' ? value : undefined;\n","import { PerformanceDistributionChart } from '../PerformanceDistributionChart/PerformanceDistributionChart';\nimport type { SimulatedPerformanceCounters } from '../PerformanceDistributionChart/types';\nimport { SectionContent } from './SectionContent';\nimport type {\n EnemMomentExamData,\n EnemMomentPerformance,\n EnemMomentPerformanceBand,\n EnemMomentSectionState,\n} from './types';\n\n/**\n * The API's tiers as the chart's counters, \"Não participou\" too. A tier the\n * API left out is a tier of nobody.\n */\nexport function toPerformanceCounters(\n bands: EnemMomentPerformanceBand[]\n): SimulatedPerformanceCounters {\n const count = (tag: EnemMomentPerformance) =>\n bands.find((band) => band.tag === tag)?.count ?? 0;\n return {\n highlight: count('HIGHLIGHT'),\n aboveAverage: count('ABOVE_AVERAGE'),\n belowAverage: count('BELOW_AVERAGE'),\n attentionPoint: count('ATTENTION_POINT'),\n notParticipated: count('NO_EXAM'),\n };\n}\n\n/**\n * \"Desempenho por quantidade de estudante\" of the unit report: the students\n * of the cut per \"Desempenho\" tier, who did not take it included.\n */\nexport function PerformanceDistributionSection({\n examData,\n}: Readonly<{ examData: EnemMomentSectionState<EnemMomentExamData> }>) {\n const bands = examData.data?.studentPerformanceBands;\n\n return (\n <SectionContent\n loading={examData.loading}\n error={examData.error}\n minHeight=\"min-h-[280px]\"\n >\n <PerformanceDistributionChart\n counters={bands ? toPerformanceCounters(bands) : undefined}\n />\n </SectionContent>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/EnemMoment/index.js","../../src/components/EnemMoment/constants.ts","../../src/components/EnemMoment/SectionContent.tsx","../../src/components/EnemMoment/ExamCards.tsx","../../src/components/EnemMoment/UnitExamDataSection.tsx","../../src/components/EnemMoment/DailyEvolutionSection.tsx","../../src/components/EnemMoment/KnowledgeAreaSection.tsx","../../src/components/EnemMoment/RankingCards.tsx","../../src/components/EnemMoment/SummaryCards.tsx","../../src/components/EnemMoment/MomentTabs.tsx","../../src/components/EnemMoment/HeaderFilters.tsx","../../src/components/EnemMoment/StudentsTable.tsx","../../src/components/EnemMoment/StudentsDownload.tsx","../../src/components/EnemMoment/tableFilters.ts","../../src/components/EnemMoment/PerformanceDistributionSection.tsx"],"names":["count","jsx","jsxs","useState","CaretUpIcon","useMemo","useCallback","useEffect","useRef"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B;AACE;AACA;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACA;AACA;AACF,uDAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B,gCAA6B;AAC7B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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/EnemMoment/index.js","sourcesContent":[null,"import type { EnemMomentEducationStage } from './types';\n\n/**\n * Options of the \"Série\" picker — the teaching stages of the classes. The\n * first is the default: the regular 3rd year is who the exams are sent to.\n */\nexport const EDUCATION_STAGE_OPTIONS: ReadonlyArray<{\n value: EnemMomentEducationStage;\n label: string;\n}> = [\n { value: 'REGULAR', label: '3ª série regular' },\n { value: 'EJA', label: 'EJA' },\n { value: 'SUBSEQUENTE', label: 'Subsequente' },\n];\n\nexport const DEFAULT_EDUCATION_STAGE: EnemMomentEducationStage = 'REGULAR';\n\n/**\n * Options of the \"Tempo de prova\" picker: each the minute its 30-minute range\n * ends at — the backend's `timeBuckets`. Separate ranges, not \"up to\": the\n * labels say both ends so nobody reads \"1h\" as \"até 1h\".\n */\nexport const EXAM_DURATION_OPTIONS: ReadonlyArray<{\n value: number;\n label: string;\n}> = [\n { value: 30, label: 'Até 30 min' },\n { value: 60, label: '30 min a 1h' },\n { value: 90, label: '1h a 1h 30min' },\n { value: 120, label: '1h 30min a 2h' },\n { value: 150, label: '2h a 2h 30min' },\n { value: 180, label: '2h 30min a 3h' },\n { value: 210, label: '3h a 3h 30min' },\n];\n\n/**\n * Color of each moment in the charts, by position: Momento 1 light blue,\n * Momento 2 orange, as in the Figma. Cycles past the fourth.\n */\nexport const MOMENT_COLORS = [\n 'var(--color-info-300)',\n 'var(--color-warning-300)',\n 'var(--color-success-300)',\n 'var(--color-error-300)',\n] as const;\n\n/** \"Detalhes por dia\" rows shown before \"Mostrar todos os dias\". */\nexport const COLLAPSED_DAY_ROWS = 6;\n","import type { ReactNode } from 'react';\nimport Text from '../Text/Text';\nimport { SkeletonCard } from '../Skeleton/Skeleton';\n\n/**\n * A section of the report while it loads (a skeleton of its height), when it\n * failed (the message, in a card of the same height), or its content.\n */\nexport function SectionContent({\n loading,\n error,\n minHeight,\n children,\n}: Readonly<{\n loading: boolean;\n error: string | null;\n /** The section's height class, kept while it loads or fails. */\n minHeight: string;\n children: ReactNode;\n}>) {\n if (loading) return <SkeletonCard className={minHeight} />;\n\n if (error) {\n return (\n <div\n className={`flex items-center justify-center ${minHeight} bg-background border border-border-50 rounded-xl p-5`}\n >\n <Text size=\"sm\" className=\"text-text-500\">\n {error}\n </Text>\n </div>\n );\n }\n\n return <>{children}</>;\n}\n","import type { ReactNode } from 'react';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport { PieChartCard, type PieSlice } from '../shared/ChartComponents';\nimport { ChatCircleIcon } from '@phosphor-icons/react/dist/csr/ChatCircle';\nimport { ClockIcon } from '@phosphor-icons/react/dist/csr/Clock';\nimport { StatCard } from '../StatisticsCard/StatisticsCard';\nimport SimpleBarChart from '../SimpleBarChart/SimpleBarChart';\nimport type {\n EnemMomentExamData,\n EnemMomentParticipation,\n EnemMomentScoreBand,\n} from './types';\nimport { formatCount, formatHoursMinutes, formatMinutesSeconds } from './utils';\n\n/**\n * The cards of the \"Dados do simulado\" block of the Momento ENEM screens: the\n * pie cards (Idioma, Participação), on the lib's PieChartCard, the Tempo card\n * and the score histogram.\n */\n\nconst CARD_CLASS =\n 'flex flex-col min-w-0 p-5 gap-4 bg-background border border-border-50 rounded-xl';\n\nconst plural = (count: number, singular: string, pluralForm: string) =>\n `${formatCount(count)} ${count === 1 ? singular : pluralForm}`;\n\nexport const students = (count: number) =>\n plural(count, 'estudante', 'estudantes');\nexport const schools = (count: number) => plural(count, 'escola', 'escolas');\n\n/** Title, optional icon and the gray line under it — every card's header. */\nfunction CardHeader({\n title,\n subtitle,\n icon,\n}: Readonly<{ title: string; subtitle: string; icon?: ReactNode }>) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-center gap-2 text-text-950\">\n {icon}\n <Text\n as=\"h3\"\n size=\"lg\"\n weight=\"bold\"\n className=\"text-text-950 tracking-[0.2px]\"\n >\n {title}\n </Text>\n </div>\n <Text size=\"sm\" className=\"text-text-600\">\n {subtitle}\n </Text>\n </div>\n );\n}\n\n/**\n * A part of a pie of students, in a CSS color — with a shorter name for the\n * slice's balloon when the legend's does not fit there.\n */\nconst slice = (\n key: string,\n label: string,\n value: number,\n color: string,\n tooltipLabel?: string\n) =>\n ({\n key,\n label,\n value,\n color,\n colorClass: '',\n ...(tooltipLabel === undefined ? {} : { tooltipLabel }),\n }) satisfies PieSlice;\n\n/**\n * Whether the Idioma card shows: the cut has a day 1, with a language to pick.\n * While the exams are not known (`null`), whether somebody picked one.\n */\nexport const showsLanguageCard = (\n hasLanguageChoice: boolean | null,\n language: EnemMomentExamData['language']\n) => hasLanguageChoice ?? language.ingles + language.espanhol > 0;\n\n/** Foreign language picked by the students who took the exam. */\nexport function LanguageCard({\n language,\n}: Readonly<{ language: EnemMomentExamData['language'] }>) {\n return (\n <PieChartCard\n title=\"Idioma\"\n subtitle=\"Língua estrangeira escolhida por quem fez a prova\"\n icon={<ChatCircleIcon size={20} aria-hidden=\"true\" />}\n emptyText=\"Ninguém fez a prova\"\n formatValue={students}\n slices={[\n // The legend has room for \"Escolheram Inglês\"; the slice's balloon\n // does not.\n slice(\n 'english',\n 'Escolheram Inglês',\n language.ingles,\n 'var(--color-info-300)',\n 'Inglês'\n ),\n slice(\n 'spanish',\n 'Escolheram Espanhol',\n language.espanhol,\n 'var(--color-indicator-positive)',\n 'Espanhol'\n ),\n ]}\n />\n );\n}\n\n/**\n * Students who took the exam and who did not. The labels are white with a\n * shadow: black would vanish on the dark \"Não participou\" slice, and the lib's\n * pie takes a single label color for every slice.\n */\nexport function ParticipationCard({\n participation,\n}: Readonly<{ participation: EnemMomentParticipation }>) {\n return (\n <PieChartCard\n title=\"Participação\"\n subtitle=\"Porcentagem de estudantes por participação no simulado\"\n icon={<ChatCircleIcon size={20} aria-hidden=\"true\" />}\n emptyText=\"Nenhum estudante neste recorte\"\n formatValue={students}\n labelColor=\"var(--color-text)\"\n labelTextShadow=\"0 0 3px rgba(0, 0, 0, 0.45)\"\n slices={[\n slice(\n 'participated',\n 'Participou',\n participation.participated,\n 'var(--color-success-200)'\n ),\n slice(\n 'notParticipated',\n 'Não participou',\n participation.notParticipated,\n 'var(--color-indicator-primary)'\n ),\n ]}\n />\n );\n}\n\n/**\n * The three times. Stacked beside other cards; alone on a row (`fullWidth`,\n * a cut with no language choice on the report) the card takes the whole row\n * and they line up.\n *\n * The exam's duration comes with the times only once somebody took it; until\n * then it is the one the exams declare (`examDurationSeconds`).\n */\nexport function TimeCard({\n time,\n examDurationSeconds,\n fullWidth = false,\n}: Readonly<{\n time: EnemMomentExamData['time'];\n examDurationSeconds: number | null;\n fullWidth?: boolean;\n}>) {\n return (\n <div className={cn(CARD_CLASS, fullWidth && 'lg:col-span-2')}>\n <CardHeader\n title=\"Tempo\"\n subtitle=\"Do início ao envio, entre os estudantes que finalizaram\"\n icon={<ClockIcon size={20} aria-hidden=\"true\" />}\n />\n <div\n className={cn('grid grid-cols-1 gap-4', fullWidth && 'md:grid-cols-3')}\n >\n <StatCard\n item={{\n label: 'Tempo médio de prova',\n value: formatHoursMinutes(time.averageTotalSeconds),\n variant: 'total',\n }}\n />\n <StatCard\n item={{\n label: 'Tempo médio por questão',\n value: formatMinutesSeconds(time.averageSecondsPerQuestion),\n variant: 'total',\n }}\n />\n <StatCard\n item={{\n label: 'Duração da prova',\n value: formatHoursMinutes(\n time.durationSeconds ?? examDurationSeconds\n ),\n variant: 'total',\n }}\n />\n </div>\n </div>\n );\n}\n\n/**\n * Participação, Idioma and Tempo side by side — a school's page and the unit\n * report. A cut where nobody picked a language (a day 2 on its own) has no\n * Idioma card, and the other two share the row.\n */\nexport function ExamCardsRow({\n participation,\n examData,\n hasLanguageChoice,\n examDurationSeconds,\n}: Readonly<{\n participation: EnemMomentParticipation;\n examData: EnemMomentExamData;\n /** Whether the cut has a day 1 — `null` while the exams are not known. */\n hasLanguageChoice: boolean | null;\n /** The cut's time limit, from its exams — `null` while they are not known. */\n examDurationSeconds: number | null;\n}>) {\n const withLanguage = showsLanguageCard(hasLanguageChoice, examData.language);\n\n return (\n <div\n className={cn(\n 'grid grid-cols-1 gap-4',\n withLanguage ? 'lg:grid-cols-3' : 'lg:grid-cols-2'\n )}\n >\n <ParticipationCard participation={participation} />\n {withLanguage && <LanguageCard language={examData.language} />}\n <TimeCard\n time={examData.time}\n examDurationSeconds={examDurationSeconds}\n />\n </div>\n );\n}\n\n/** A score histogram, with its total under the title (\"200 estudantes\"). */\nexport function ScoreBandChart({\n title,\n bands,\n formatTotal,\n barColor,\n}: Readonly<{\n title: string;\n bands: EnemMomentScoreBand[];\n formatTotal: (total: number) => string;\n barColor: string;\n}>) {\n const total = bands.reduce((sum, band) => sum + band.count, 0);\n\n return (\n <SimpleBarChart\n title={title}\n subtitle={formatTotal(total)}\n data={bands.map((band) => ({ label: band.label, value: band.count }))}\n barColor={barColor}\n // The axis shows the band alone (\"2 a 4\"), which says neither what it\n // bands nor how many; the balloon has room for the whole sentence.\n formatTooltip={(item) =>\n `Média de ${item.label}: ${formatTotal(item.value)}`\n }\n />\n );\n}\n","import Text from '../Text/Text';\nimport { SectionContent } from './SectionContent';\nimport { ExamCardsRow, ScoreBandChart, students } from './ExamCards';\nimport type {\n EnemMomentExamData,\n EnemMomentSectionState,\n EnemMomentSummary,\n} from './types';\n\n/**\n * \"Dados do simulado\" of the unit report (Gestor de Unidade, professor): how its students scored, then\n * Participação, Idioma and Tempo in a row.\n *\n * One school, so no histogram of school averages; Participação splits the\n * students of the cards, who took the exam and who did not.\n */\nexport function UnitExamDataSection({\n summary,\n examData,\n hasLanguageChoice,\n examDurationSeconds,\n}: Readonly<{\n summary: EnemMomentSectionState<EnemMomentSummary>;\n examData: EnemMomentSectionState<EnemMomentExamData>;\n /** Whether the cut has a day 1 — `null` while the exams are not known. */\n hasLanguageChoice: boolean | null;\n /** The cut's time limit, from its exams — `null` while they are not known. */\n examDurationSeconds: number | null;\n}>) {\n return (\n <section className=\"flex flex-col gap-4\">\n <Text as=\"h2\" size=\"xl\" weight=\"bold\" className=\"text-text-950\">\n Dados do simulado\n </Text>\n <SectionContent\n loading={summary.loading || examData.loading}\n error={summary.error ?? examData.error}\n minHeight=\"min-h-[520px]\"\n >\n {summary.data && examData.data && (\n <div className=\"flex flex-col gap-4\">\n <ScoreBandChart\n title=\"Estudantes por faixa de nota\"\n bands={examData.data.studentScoreBands}\n formatTotal={students}\n barColor=\"bg-info-500\"\n />\n <ExamCardsRow\n participation={{\n participated: summary.data.participatingStudents,\n notParticipated: summary.data.studentsWithoutParticipation,\n }}\n examData={examData.data}\n hasLanguageChoice={hasLanguageChoice}\n examDurationSeconds={examDurationSeconds}\n />\n </div>\n )}\n </SectionContent>\n </section>\n );\n}\n","import { useMemo, useState } from 'react';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport Button from '../Button/Button';\nimport { Tooltip } from '../Tooltip/Tooltip';\nimport {\n YAxis,\n GridLines,\n calculateYAxisTicks,\n} from '../shared/ChartComponents';\nimport {\n TableProvider,\n type ColumnConfig,\n type TableParams,\n} from '../TableProvider/TableProvider';\nimport { CaretDownIcon } from '@phosphor-icons/react/dist/csr/CaretDown';\nimport { CaretUpIcon } from '@phosphor-icons/react/dist/csr/CaretUp';\nimport { SectionContent } from './SectionContent';\nimport { COLLAPSED_DAY_ROWS, MOMENT_COLORS } from './constants';\nimport type {\n EnemMomentDailyEvolution,\n EnemMomentDay,\n EnemMomentMoment,\n EnemMomentSectionState,\n} from './types';\nimport { formatCount, formatScore, MISSING_VALUE } from './utils';\n\nconst CHART_HEIGHT = 180;\n\n/** `YYYY-MM-DD` → `DD/MM`. */\nconst shortDate = (date: string) => {\n const [, month, day] = date.slice(0, 10).split('-');\n return `${day}/${month}`;\n};\n\nconst simulados = (count: number) =>\n `${formatCount(count)} ${count === 1 ? 'simulado' : 'simulados'}`;\n\n/** A moment's color and name, and a fallback for an exam the tabs lack. */\nfunction useMomentLookup(moments: EnemMomentMoment[]) {\n return useMemo(() => {\n const byExam = new Map(\n moments.map((moment, index) => [\n moment.examId,\n { ...moment, color: MOMENT_COLORS[index % MOMENT_COLORS.length] },\n ])\n );\n return (examId: string) =>\n byExam.get(examId) ?? {\n examId,\n label: 'Momento',\n color: MOMENT_COLORS[0],\n };\n }, [moments]);\n}\n\n/**\n * \"Simulados realizados por dia\": one bar per day with submissions, split in\n * the color of each moment submitted that day, with the total and each\n * moment's share above.\n */\nfunction DailyChart({\n days,\n moments,\n}: Readonly<{ days: EnemMomentDay[]; moments: EnemMomentMoment[] }>) {\n const momentOf = useMomentLookup(moments);\n\n const totalsByExam = new Map<string, number>();\n for (const day of days) {\n for (const exam of day.byExam) {\n totalsByExam.set(\n exam.examId,\n (totalsByExam.get(exam.examId) ?? 0) + exam.participations\n );\n }\n }\n const total = days.reduce((sum, day) => sum + day.participations, 0);\n // Only the moments of the cut: a Momento tab lists one.\n const legend = moments.filter((moment) => totalsByExam.has(moment.examId));\n\n const ticks = calculateYAxisTicks(\n Math.max(0, ...days.map((day) => day.participations))\n );\n const max = ticks[0];\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Simulados realizados por dia\n </Text>\n <Badge\n variant=\"solid\"\n action=\"info\"\n size=\"small\"\n className=\"self-start uppercase\"\n >\n {`${simulados(total)} ${total === 1 ? 'total' : 'totais'}`}\n </Badge>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-2\">\n {legend.map((moment) => (\n <div key={moment.examId} className=\"flex items-center gap-2\">\n <span\n className=\"w-2 h-2 rounded-full shrink-0\"\n style={{ backgroundColor: momentOf(moment.examId).color }}\n />\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-950\">\n {moment.label}\n </Text>\n <Text size=\"sm\" className=\"text-text-600\">\n {simulados(totalsByExam.get(moment.examId) ?? 0)}\n </Text>\n </div>\n ))}\n </div>\n\n <div className=\"flex flex-row min-w-0\">\n <YAxis ticks={ticks} chartHeight={CHART_HEIGHT} />\n <div className=\"flex-1 min-w-0 relative\">\n <GridLines ticks={ticks} chartHeight={CHART_HEIGHT} />\n <div className=\"flex flex-row gap-1 sm:gap-2 relative z-10\">\n {days.map((day) => (\n <div\n key={day.day}\n className=\"flex-1 min-w-0 flex flex-col items-center gap-2\"\n >\n <div\n data-testid={`day-bar-${day.day}`}\n aria-label={`${shortDate(day.day)}: ${simulados(day.participations)}`}\n className=\"w-full max-w-8 flex flex-col-reverse rounded-md overflow-hidden\"\n style={{ height: CHART_HEIGHT }}\n >\n {day.byExam\n .filter((exam) => exam.participations > 0)\n .map((exam) => {\n const moment = momentOf(exam.examId);\n const height =\n max === 0\n ? 0\n : (exam.participations / max) * CHART_HEIGHT;\n return (\n <Tooltip\n key={exam.examId}\n content={`${moment.label} · ${simulados(exam.participations)}`}\n position=\"top\"\n className=\"w-full flex\"\n // The bar box clips its content to keep the stacked\n // segments inside one rounded rectangle, and a\n // balloon rendered in place is clipped with them:\n // what reached the screen was a sliver of text as\n // wide as the bar. The portal takes it out of there.\n usePortal\n >\n <div\n data-testid={`day-bar-${day.day}-${exam.examId}`}\n className=\"w-full\"\n style={{\n height: `${height}px`,\n backgroundColor: moment.color,\n }}\n />\n </Tooltip>\n );\n })}\n </div>\n <Text\n size=\"2xs\"\n weight=\"medium\"\n className=\"w-full text-text-600 text-center leading-tight sm:text-xs\"\n >\n {`Dia ${shortDate(day.day)}`}\n </Text>\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\ntype DayRow = {\n rowId: string;\n /** For sorting: the date, `YYYY-MM-DD`. */\n date: string;\n /** \"Dia N · DD/MM\" — what the search matches. */\n label: string;\n dayNumber: number;\n participations: number;\n accumulatedParticipations: number;\n accumulatedAverageScore: number | null;\n};\n\nconst DAY_COLUMNS: ColumnConfig<DayRow>[] = [\n {\n key: 'date',\n label: 'Dia',\n sortable: true,\n className: 'min-w-[200px]',\n render: (_value, row) => (\n <Text size=\"sm\" className=\"text-text-800\">\n {`Dia ${row.dayNumber} · `}\n <Text as=\"span\" size=\"sm\" weight=\"bold\" className=\"text-text-950\">\n {shortDate(row.date)}\n </Text>\n </Text>\n ),\n },\n {\n key: 'participations',\n label: 'Provas realizadas',\n sortable: false,\n render: (_value, row) => formatCount(row.participations),\n },\n {\n key: 'accumulatedParticipations',\n label: 'Acumulado',\n sortable: false,\n render: (_value, row) => formatCount(row.accumulatedParticipations),\n },\n {\n key: 'accumulatedAverageScore',\n label: 'Nota média acumulada',\n sortable: false,\n render: (_value, row) =>\n row.accumulatedAverageScore === null\n ? MISSING_VALUE\n : formatScore(row.accumulatedAverageScore),\n },\n];\n\n/**\n * \"Detalhes por dia\": the days with submissions, searchable and sortable by date,\n * collapsed to the first rows behind \"Mostrar todos os dias\".\n *\n * \"Dia N\" counts the days of the cut in date order, so it does not change when\n * the table is sorted or searched.\n */\nfunction DaysTable({ days }: Readonly<{ days: EnemMomentDay[] }>) {\n const [search, setSearch] = useState('');\n const [order, setOrder] = useState<'asc' | 'desc'>('asc');\n const [expanded, setExpanded] = useState(false);\n\n const allRows = useMemo<DayRow[]>(\n () =>\n days.map((day, index) => ({\n rowId: day.day,\n date: day.day,\n label: `Dia ${index + 1} · ${shortDate(day.day)}`,\n dayNumber: index + 1,\n participations: day.participations,\n accumulatedParticipations: day.accumulatedParticipations,\n accumulatedAverageScore: day.accumulatedAverageScore,\n })),\n [days]\n );\n\n const rows = useMemo(() => {\n const term = search.trim().toLocaleLowerCase('pt-BR');\n const matching = term\n ? allRows.filter((row) =>\n row.label.toLocaleLowerCase('pt-BR').includes(term)\n )\n : allRows;\n return order === 'asc' ? matching : [...matching].reverse();\n }, [allRows, search, order]);\n\n const visibleRows = expanded ? rows : rows.slice(0, COLLAPSED_DAY_ROWS);\n const hasMore = rows.length > COLLAPSED_DAY_ROWS;\n\n // Server mode: the table only reports search and sort, and they are applied\n // here to every day before the collapse cut — not to the rows on screen.\n const handleParamsChange = (params: TableParams) => {\n setSearch(params.search ?? '');\n setOrder(params.sortOrder === 'desc' ? 'desc' : 'asc');\n };\n\n return (\n <div className=\"flex flex-col items-center gap-4\">\n <TableProvider<DayRow>\n tableId=\"enemMomentDays\"\n data={visibleRows}\n headers={DAY_COLUMNS}\n rowKey=\"rowId\"\n variant=\"borderless\"\n enableSearch\n enableTableSort\n sortMode=\"server\"\n defaultSort={{ sortBy: 'date', sortOrder: 'asc' }}\n onParamsChange={handleParamsChange}\n searchPlaceholder=\"Buscar\"\n headerContent={\n <div className=\"flex flex-col gap-2\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Detalhes por dia\n </Text>\n <Badge\n variant=\"solid\"\n action=\"info\"\n size=\"small\"\n className=\"self-start uppercase\"\n >\n {`${allRows.length} ${allRows.length === 1 ? 'dia total' : 'dias totais'}`}\n </Badge>\n </div>\n }\n />\n {hasMore && (\n <Button\n variant=\"outline\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setExpanded((previous) => !previous)}\n >\n {expanded ? 'Mostrar menos' : 'Mostrar todos os dias'}\n {expanded ? <CaretUpIcon size={18} /> : <CaretDownIcon size={18} />}\n </Button>\n )}\n </div>\n );\n}\n\n/**\n * \"Evolução por dia\": the exams submitted each day — as bars in the color of\n * their moment, then day by day in a table. Only the days with submissions:\n * the moments run weeks apart, and the days between them would be a row of\n * empty bars.\n */\nexport function DailyEvolutionSection({\n daily,\n moments,\n}: Readonly<{\n daily: EnemMomentSectionState<EnemMomentDailyEvolution>;\n moments: EnemMomentMoment[];\n}>) {\n // Date order, whatever order the API sends: the bars read left to right and\n // \"Dia N\" counts from the first day.\n const days = useMemo(\n () =>\n [...(daily.data?.days ?? [])].sort((a, b) => a.day.localeCompare(b.day)),\n [daily.data]\n );\n\n return (\n <section className=\"flex flex-col gap-4\">\n <Text as=\"h2\" size=\"xl\" weight=\"bold\" className=\"text-text-950\">\n Evolução por dia\n </Text>\n <SectionContent\n loading={daily.loading}\n error={daily.error}\n minHeight=\"min-h-[480px]\"\n >\n <div className=\"flex flex-col gap-6 p-5 bg-background border border-border-50 rounded-xl\">\n {days.length === 0 ? (\n <div className=\"flex items-center justify-center min-h-[200px]\">\n <Text size=\"sm\" className=\"text-text-500\">\n Ainda não há provas entregues neste recorte.\n </Text>\n </div>\n ) : (\n <>\n <DailyChart days={days} moments={moments} />\n <div className=\"border-t border-border-100\" />\n <DaysTable days={days} />\n </>\n )}\n </div>\n </SectionContent>\n </section>\n );\n}\n","import { useMemo, useState } from 'react';\nimport Text from '../Text/Text';\nimport Badge from '../Badge/Badge';\nimport Button from '../Button/Button';\nimport Select, {\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n} from '../Select/Select';\nimport {\n QuestionsBars,\n RateCell,\n type QuestionsBarsValues,\n} from '../QuestionsPerformanceCard';\nimport { StatCard } from '../StatisticsCard/StatisticsCard';\nimport {\n TableProvider,\n type ColumnConfig,\n type TableParams,\n} from '../TableProvider/TableProvider';\nimport { renderSubjectsCell } from '../../utils/renderSubjectCell';\nimport { CaretDownIcon } from '@phosphor-icons/react/dist/csr/CaretDown';\nimport { CaretUpIcon } from '@phosphor-icons/react/dist/csr/CaretUp';\nimport { SectionContent } from './SectionContent';\nimport type {\n EnemMomentQuestionsView,\n EnemMomentSectionState,\n EnemMomentSubjectRow,\n} from './types';\nimport { formatCount, formatScore, MISSING_VALUE } from './utils';\n\n/** Value of the select meaning every area. */\nconst ALL_AREAS = 'all';\n\n/** Componentes shown before \"Mostrar todos\". */\nconst COLLAPSED_SUBJECT_ROWS = 4;\n\ntype SubjectRow = {\n rowId: string;\n name: string;\n /** What `renderSubjectsCell` draws: the chip and the name. */\n subject: Array<{ id: string; name: string; color: string; icon: string }>;\n areaName: string;\n total: number;\n correct: number;\n incorrect: number;\n blank: number;\n /** `null` when nobody answered its questions. */\n rate: number | null;\n};\n\ntype SubjectSortKey =\n | 'name'\n | 'areaName'\n | 'correct'\n | 'incorrect'\n | 'blank'\n | 'rate';\n\nconst SORT_KEYS: ReadonlySet<string> = new Set<SubjectSortKey>([\n 'name',\n 'areaName',\n 'correct',\n 'incorrect',\n 'blank',\n 'rate',\n]);\n\nconst SUBJECT_COLUMNS: ColumnConfig<SubjectRow>[] = [\n {\n key: 'name',\n label: 'Componente curricular',\n sortable: true,\n className: 'min-w-[200px]',\n render: (_value, row) => renderSubjectsCell(row.subject),\n },\n {\n key: 'areaName',\n label: 'Área do conhecimento',\n sortable: true,\n className: 'min-w-[240px]',\n },\n {\n key: 'total',\n label: 'Total',\n sortable: false,\n render: (_value, row) => formatCount(row.total),\n },\n {\n key: 'correct',\n label: 'Corretas',\n sortable: true,\n render: (_value, row) => formatCount(row.correct),\n },\n {\n key: 'incorrect',\n label: 'Incorretas',\n sortable: true,\n render: (_value, row) => formatCount(row.incorrect),\n },\n {\n key: 'blank',\n label: 'Em branco',\n sortable: true,\n render: (_value, row) => formatCount(row.blank),\n },\n {\n key: 'rate',\n label: 'Taxa de acerto',\n sortable: true,\n render: (_value, row) =>\n row.rate === null ? MISSING_VALUE : <RateCell rate={row.rate} />,\n },\n];\n\n/** Sort rows by one column; names in pt-BR order. */\nfunction sortRows(\n rows: SubjectRow[],\n key: SubjectSortKey,\n order: 'asc' | 'desc'\n): SubjectRow[] {\n const direction = order === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const result =\n key === 'name' || key === 'areaName'\n ? a[key].localeCompare(b[key], 'pt-BR')\n : (a[key] ?? -1) - (b[key] ?? -1);\n return result * direction;\n });\n}\n\n/**\n * \"Desempenho por componente curricular\": searchable (by componente or area)\n * and sortable, collapsed to the first rows behind \"Mostrar todos\". Until a\n * column is sorted, the componentes keep the API's order: worst hit rate\n * first.\n */\nfunction SubjectsTable({\n subjects,\n}: Readonly<{ subjects: EnemMomentSubjectRow[] }>) {\n const [search, setSearch] = useState('');\n const [sort, setSort] = useState<{\n key: SubjectSortKey;\n order: 'asc' | 'desc';\n } | null>(null);\n const [expanded, setExpanded] = useState(false);\n\n const rows = useMemo(() => {\n const term = search.trim().toLocaleLowerCase('pt-BR');\n const all: SubjectRow[] = subjects.map((subject) => ({\n rowId: subject.subjectId,\n name: subject.subjectName,\n subject: [\n {\n id: subject.subjectId,\n name: subject.subjectName,\n color: subject.color,\n icon: subject.icon,\n },\n ],\n areaName: subject.areaKnowledgeName,\n total: subject.answered,\n correct: subject.correct,\n incorrect: subject.incorrect,\n blank: subject.blank,\n rate: subject.correctPercentage,\n }));\n const matching = term\n ? all.filter((row) =>\n `${row.name} ${row.areaName}`\n .toLocaleLowerCase('pt-BR')\n .includes(term)\n )\n : all;\n return sort ? sortRows(matching, sort.key, sort.order) : matching;\n }, [subjects, search, sort]);\n\n const visibleRows = expanded ? rows : rows.slice(0, COLLAPSED_SUBJECT_ROWS);\n const hasMore = rows.length > COLLAPSED_SUBJECT_ROWS;\n\n // Server mode: the table only reports search and sort, and they are applied\n // here to every componente before the collapse cut.\n const handleParamsChange = (params: TableParams) => {\n setSearch(params.search ?? '');\n const key = params.sortBy;\n setSort(\n key && SORT_KEYS.has(key)\n ? {\n key: key as SubjectSortKey,\n order: params.sortOrder === 'desc' ? 'desc' : 'asc',\n }\n : null\n );\n };\n\n const noun = (count: number) =>\n count === 1 ? 'componente curricular' : 'componentes curriculares';\n\n return (\n <div className=\"flex flex-col items-center gap-4\">\n <TableProvider<SubjectRow>\n tableId=\"enemMomentSubjects\"\n data={visibleRows}\n headers={SUBJECT_COLUMNS}\n rowKey=\"rowId\"\n variant=\"borderless\"\n enableSearch\n enableTableSort\n sortMode=\"server\"\n onParamsChange={handleParamsChange}\n searchPlaceholder=\"Buscar\"\n headerContent={\n <div className=\"flex flex-col gap-2\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Desempenho por componente curricular\n </Text>\n <Badge\n variant=\"solid\"\n action=\"info\"\n size=\"small\"\n className=\"self-start uppercase\"\n >\n {`${subjects.length} ${noun(subjects.length)} ${\n subjects.length === 1 ? 'total' : 'totais'\n }`}\n </Badge>\n </div>\n }\n />\n {hasMore && (\n <Button\n variant=\"outline\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setExpanded((previous) => !previous)}\n >\n {expanded\n ? 'Mostrar menos'\n : `Mostrar todos os ${rows.length} ${noun(rows.length)}`}\n {expanded ? <CaretUpIcon size={18} /> : <CaretDownIcon size={18} />}\n </Button>\n )}\n </div>\n );\n}\n\n/**\n * The card's body: the area select narrows the bars, the score and the table\n * together.\n *\n * The counts are the API's, never summed from the table: a question mapped to\n * two componentes counts in both rows but once in its area and in the totals.\n * The score is the one derived from the hit rate, for every area and for all\n * of them — the corrected score of the summary would jump against the areas'.\n */\nfunction QuestionsCard({ data }: Readonly<{ data: EnemMomentQuestionsView }>) {\n const [areaId, setAreaId] = useState(ALL_AREAS);\n\n // The select lists them in pt-BR order; the API sends worst first.\n const areas = useMemo(\n () =>\n [...data.areas].sort((a, b) =>\n a.areaKnowledgeName.localeCompare(b.areaKnowledgeName, 'pt-BR')\n ),\n [data.areas]\n );\n // An area that vanished with a new cut falls back to \"all\" on read.\n const area = areas.find((item) => item.areaKnowledgeId === areaId) ?? null;\n\n const subjects = useMemo(\n () =>\n area\n ? data.subjects.filter(\n (subject) => subject.areaKnowledgeId === area.areaKnowledgeId\n )\n : data.subjects,\n [data.subjects, area]\n );\n\n const shown = area ?? data.totals;\n const values: QuestionsBarsValues = {\n total: shown.answered,\n corretas: shown.correct,\n incorretas: shown.incorrect,\n emBranco: shown.blank,\n };\n\n return (\n <div className=\"flex flex-col gap-4 rounded-xl border border-border-50 bg-background p-5\">\n <div className=\"flex flex-row flex-wrap items-center justify-between gap-4\">\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Dados de questões\n </Text>\n <Select\n value={area ? area.areaKnowledgeId : ALL_AREAS}\n onValueChange={setAreaId}\n className=\"w-[380px] max-w-full\"\n >\n <SelectTrigger className=\"w-full\" aria-label=\"Área do conhecimento\">\n <SelectValue placeholder=\"Todas as áreas do conhecimento\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_AREAS}>\n Todas as áreas do conhecimento\n </SelectItem>\n {areas.map((item) => (\n <SelectItem\n key={item.areaKnowledgeId}\n value={item.areaKnowledgeId}\n >\n {item.areaKnowledgeName}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <QuestionsBars\n values={values}\n aside={\n <StatCard\n item={{\n label: 'Nota média',\n value:\n shown.averageScore === null\n ? MISSING_VALUE\n : formatScore(shown.averageScore),\n variant: 'total',\n }}\n />\n }\n />\n\n <div aria-hidden=\"true\" className=\"h-px w-full bg-border-200\" />\n\n <SubjectsTable subjects={subjects} />\n </div>\n );\n}\n\n/**\n * \"Desempenho por área de conhecimento\": how the answers split — overall or\n * for one area — and how each componente curricular did.\n */\nexport function KnowledgeAreaSection({\n questions,\n}: Readonly<{\n questions: EnemMomentSectionState<EnemMomentQuestionsView>;\n}>) {\n return (\n <section className=\"flex flex-col gap-4\">\n <Text as=\"h2\" size=\"xl\" weight=\"bold\" className=\"text-text-950\">\n Desempenho por área de conhecimento\n </Text>\n <SectionContent\n loading={questions.loading}\n error={questions.error}\n minHeight=\"min-h-[560px]\"\n >\n {questions.data && <QuestionsCard data={questions.data} />}\n </SectionContent>\n </section>\n );\n}\n","import type { ReactNode } from 'react';\nimport { MedalIcon } from '@phosphor-icons/react/dist/csr/Medal';\nimport { SealWarningIcon } from '@phosphor-icons/react/dist/csr/SealWarning';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport {\n RankingCard,\n type StudentRankingItem,\n type StudentRankingVariant,\n} from '../StudentRanking/StudentRanking';\nimport { SectionContent } from './SectionContent';\nimport type { EnemMomentSectionState, EnemMomentStudentRow } from './types';\nimport { formatScore } from './utils';\n\n/** Students each ranking card lists. */\nexport const RANKING_ROWS = 3;\n\n/** Both ranking cards, when nobody of the cut has a score. */\nexport const EMPTY_RANKING_TEXT =\n 'Nenhum estudante finalizou o simulado neste recorte.';\n\n/** A student of a ranking, with their position in it. */\nexport type EnemMomentRankedStudent = EnemMomentStudentRow & {\n position: number;\n averageScore: number;\n};\n\n/** \"Nota 9,0\" where the lib's row prints a percentage, in the card's color. */\nexport function ScoreBadge({\n score,\n variant,\n}: Readonly<{ score: number; variant: StudentRankingVariant }>) {\n return (\n <Text\n size=\"xs\"\n weight=\"medium\"\n className={cn(\n 'shrink-0 flex items-center h-5.5 px-2 rounded bg-background',\n variant === 'highlight' ? 'text-success-700' : 'text-error-700'\n )}\n >\n {`Nota ${formatScore(score)}`}\n </Text>\n );\n}\n\n/**\n * A ranking card of the report — the lib's, with its loading and error, whose\n * row click hands back the student of that row.\n */\nexport function EnemMomentRankingCard({\n title,\n variant,\n ranking,\n toItem,\n headerIcon,\n emptyText = EMPTY_RANKING_TEXT,\n footer,\n onStudentClick,\n}: Readonly<{\n title: string;\n variant: StudentRankingVariant;\n ranking: EnemMomentSectionState<EnemMomentRankedStudent[]>;\n toItem: (student: EnemMomentRankedStudent) => StudentRankingItem;\n headerIcon: ReactNode;\n emptyText?: string;\n footer?: ReactNode;\n /** Opens the student's modal. Without it the rows are not clickable. */\n onStudentClick?: (student: EnemMomentRankedStudent) => void;\n}>) {\n const top = (ranking.data ?? []).slice(0, RANKING_ROWS);\n\n // The card hands back the row it drew, so the student comes from the id it\n // carries — the enrolment, which is what the modal is fetched by.\n const handleClick = onStudentClick\n ? (item: StudentRankingItem) => {\n const student = top.find(\n (candidate) => candidate.userInstitutionId === item.id\n );\n if (student) onStudentClick(student);\n }\n : undefined;\n\n return (\n <SectionContent\n loading={ranking.loading}\n error={ranking.error}\n minHeight=\"min-h-[254px]\"\n >\n <RankingCard\n title={title}\n variant={variant}\n students={top.map(toItem)}\n onStudentClick={handleClick}\n headerIcon={headerIcon}\n emptyText={emptyText}\n footer={footer}\n />\n </SectionContent>\n );\n}\n\n/**\n * A student of the unit's rankings: the name alone — the school is the\n * report's — and the score in the card's color.\n */\nconst toUnitRankingItem =\n (variant: StudentRankingVariant) =>\n (student: EnemMomentRankedStudent): StudentRankingItem => ({\n id: student.userInstitutionId,\n position: student.position,\n name: student.studentName,\n percentage: student.hitRate ?? 0,\n badge: <ScoreBadge score={student.averageScore} variant={variant} />,\n });\n\nconst toHighlightItem = toUnitRankingItem('highlight');\nconst toAttentionItem = toUnitRankingItem('attention');\n\n/**\n * \"Estudantes em destaque\" and \"Estudantes com maior dificuldade\" side by\n * side: the top and the bottom three scores of the unit's cut.\n */\nexport function UnitStudentRankings({\n highlights,\n struggling,\n onStudentClick,\n}: Readonly<{\n highlights: EnemMomentSectionState<EnemMomentRankedStudent[]>;\n struggling: EnemMomentSectionState<EnemMomentRankedStudent[]>;\n /** Opens the student's modal. Without it the rows are not clickable. */\n onStudentClick?: (student: EnemMomentRankedStudent) => void;\n}>) {\n return (\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <EnemMomentRankingCard\n title=\"Estudantes em destaque\"\n variant=\"highlight\"\n ranking={highlights}\n toItem={toHighlightItem}\n onStudentClick={onStudentClick}\n headerIcon={\n <MedalIcon size={14} className=\"text-text-950\" aria-hidden=\"true\" />\n }\n />\n <EnemMomentRankingCard\n title=\"Estudantes com maior dificuldade\"\n variant=\"attention\"\n ranking={struggling}\n toItem={toAttentionItem}\n onStudentClick={onStudentClick}\n headerIcon={\n <SealWarningIcon size={14} className=\"text-text\" aria-hidden=\"true\" />\n }\n />\n </div>\n );\n}\n","import { useMemo } from 'react';\nimport { ExamIcon } from '@phosphor-icons/react/dist/csr/Exam';\nimport { NavigationArrowIcon } from '@phosphor-icons/react/dist/csr/NavigationArrow';\nimport { ProhibitIcon } from '@phosphor-icons/react/dist/csr/Prohibit';\nimport { StudentIcon } from '@phosphor-icons/react/dist/csr/Student';\nimport {\n TimeReport,\n type TimeCardData,\n type TimeReportTab,\n} from '../TimeReport/TimeReport';\nimport { SectionContent } from './SectionContent';\nimport type { EnemMomentSectionState, EnemMomentSummary } from './types';\nimport { formatCount, formatPercentage } from './utils';\n\nconst plural = (count: number, singular: string, pluralForm: string) =>\n `${formatCount(count)} ${count === 1 ? singular : pluralForm}`;\n\n/**\n * The four cards of the unit report (Gestor de Unidade), all counting\n * students of its school:\n *\n * Card 1 — Total de estudantes : totalStudents | \"1 escola • 134 turmas\"\n * Card 2 — Realizaram o simulado : participatingStudents | \"de N estudantes\"\n * Card 3 — % de realização : studentParticipationPercentage | \"N ainda não realizaram\"\n * Card 4 — Estudantes sem realizar o simulado : studentsWithoutParticipation | \"não realizaram\"\n */\nexport function buildEnemMomentUnitSummaryCards(\n summary: EnemMomentSummary\n): TimeCardData[] {\n const where = `${plural(summary.totalSchools, 'escola', 'escolas')} • ${plural(summary.totalClasses, 'turma', 'turmas')}`;\n\n return [\n {\n id: 'totalStudents',\n label: 'TOTAL DE ESTUDANTES',\n value: formatCount(summary.totalStudents),\n icon: <StudentIcon />,\n footer: where,\n },\n {\n id: 'participatingStudents',\n label: 'REALIZARAM O SIMULADO',\n value: formatCount(summary.participatingStudents),\n icon: <ExamIcon />,\n footer: `de ${plural(summary.totalStudents, 'estudante', 'estudantes')}`,\n },\n {\n id: 'studentParticipationPercentage',\n label: '% DE REALIZAÇÃO',\n value: formatPercentage(summary.studentParticipationPercentage),\n icon: <NavigationArrowIcon />,\n footer: `${formatCount(summary.studentsWithoutParticipation)} ainda não realizaram`,\n },\n {\n id: 'studentsWithoutParticipation',\n label: 'ESTUDANTES SEM REALIZAR O SIMULADO',\n value: formatCount(summary.studentsWithoutParticipation),\n icon: <ProhibitIcon />,\n footer: 'não realizaram',\n },\n ];\n}\n\n/** The cards block has no profile tabs: a single, stripless one. */\nconst SUMMARY_TAB_VALUE = 'enem-moment';\n\n/**\n * The row of four cards at the top of the report, on the lib's TimeReport —\n * the network's cards or the unit's (`buildCards`).\n */\nexport function EnemMomentSummaryCards({\n summary,\n buildCards,\n}: Readonly<{\n summary: EnemMomentSectionState<EnemMomentSummary>;\n buildCards: (summary: EnemMomentSummary) => TimeCardData[];\n}>) {\n const tabs = useMemo<TimeReportTab[]>(\n () => [\n {\n value: SUMMARY_TAB_VALUE,\n label: '',\n cards: summary.data ? buildCards(summary.data) : [],\n },\n ],\n [summary.data, buildCards]\n );\n\n return (\n <SectionContent\n loading={summary.loading}\n error={summary.error}\n minHeight=\"min-h-[140px]\"\n >\n <TimeReport tabs={tabs} activeTab={SUMMARY_TAB_VALUE} />\n </SectionContent>\n );\n}\n","import { ReportTabs } from '../shared/ReportTabs';\n\n/** One tab of the report: \"Geral\" or one exam. */\nexport interface EnemMomentTab {\n value: string;\n label: string;\n}\n\n/**\n * The \"Geral\" / \"Momento N\" strip of the Momento ENEM screens — the lib's\n * report tabs, the look of the profile tabs of the other reports. Only the\n * active tab prints: the others are navigation, not content.\n *\n * Not drawn with \"Geral\" plus a single exam: two tabs showing the same numbers.\n */\nexport function MomentTabs({\n tabs,\n activeTab,\n onTabChange,\n}: Readonly<{\n tabs: EnemMomentTab[];\n activeTab: string;\n onTabChange: (value: string) => void;\n}>) {\n if (tabs.length <= 2) return null;\n\n return (\n <ReportTabs\n tabs={tabs}\n value={activeTab}\n onValueChange={onTabChange}\n printActiveOnly\n />\n );\n}\n","import { ClockIcon } from '@phosphor-icons/react/dist/csr/Clock';\nimport Select, {\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n} from '../Select/Select';\nimport { MultiCheckSelect } from '../MultiCheckSelect/MultiCheckSelect';\nimport { EDUCATION_STAGE_OPTIONS, EXAM_DURATION_OPTIONS } from './constants';\nimport type { EnemMomentEducationStage } from './types';\n\nconst formatSelectedDurations = (count: number) =>\n count === 1 ? '1 tempo selecionado' : `${count} tempos selecionados`;\n\n/**\n * The picks of the Momento ENEM screens: \"Tempo de prova\" and the teaching\n * stage of the classes. They sit in the page's header, in place of the period\n * and filters of the other reports.\n */\nexport function EnemMomentHeaderFilters({\n durations,\n onDurationsChange,\n educationStage,\n onEducationStageChange,\n}: Readonly<{\n /** \"Tempo de prova\" picks, in minutes; empty is every time. */\n durations: number[];\n onDurationsChange: (durations: number[]) => void;\n educationStage: EnemMomentEducationStage;\n onEducationStageChange: (stage: EnemMomentEducationStage) => void;\n}>) {\n return (\n <>\n <MultiCheckSelect\n options={EXAM_DURATION_OPTIONS}\n values={durations}\n onValuesChange={onDurationsChange}\n placeholder=\"Tempo de prova\"\n allLabel=\"Todos os tempos\"\n formatSelected={formatSelectedDurations}\n icon={<ClockIcon size={18} aria-hidden=\"true\" />}\n aria-label=\"Tempo de prova\"\n />\n\n {/* The width goes on the root, not only on the trigger: the root is\n `w-full`, and in a header that wraps (the gestor's) a full-width item\n drops to a line of its own, far from \"Tempo de prova\". */}\n <Select\n value={educationStage}\n onValueChange={(value) =>\n onEducationStageChange(value as EnemMomentEducationStage)\n }\n className=\"w-auto\"\n >\n <SelectTrigger className=\"w-auto min-w-45 gap-2\" aria-label=\"Série\">\n <SelectValue placeholder=\"Série\" />\n </SelectTrigger>\n <SelectContent>\n {EDUCATION_STAGE_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </>\n );\n}\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport Text from '../Text/Text';\nimport { UserIcon } from '../UserIcon/UserIcon';\nimport { cn } from '../../utils/utils';\nimport {\n TableProvider,\n type ColumnConfig,\n type TableComponents,\n type TableParams,\n} from '../TableProvider/TableProvider';\nimport { RateCell } from '../QuestionsPerformanceCard';\nimport { SectionContent } from './SectionContent';\nimport { StudentsTableDownload } from './StudentsDownload';\nimport {\n ENEM_MOMENT_PERFORMANCE_LABELS,\n ENEM_MOMENT_PERFORMANCE_ORDER,\n EnemMomentPerformanceBadge,\n ParticipationBadge,\n type EnemMomentStudentParticipation,\n} from './PerformanceBadge';\nimport type {\n EnemMomentFilterOptions,\n EnemMomentPagination,\n EnemMomentParticipationFilter,\n EnemMomentPerformance,\n EnemMomentStudentRow,\n EnemMomentStudentsExport,\n EnemMomentStudentsOrderBy,\n EnemMomentStudentsQuery,\n} from './types';\nimport { classOptionLabel, listFilter, pickedValue } from './tableFilters';\nimport { formatClock, formatCount, formatScore, MISSING_VALUE } from './utils';\n\nexport function participationOf(\n row: EnemMomentStudentRow\n): EnemMomentStudentParticipation {\n if (row.participatedExams === 0) return 'NOT_PARTICIPATED';\n return row.partialParticipation ? 'PARTIAL' : 'PARTICIPATED';\n}\n\nconst PARTICIPATION_COLUMN: ColumnConfig<EnemMomentStudentRow> = {\n key: 'participatedExams',\n label: 'Participação',\n filter: {\n paramKey: 'participation',\n allLabel: 'Todos',\n options: [\n {\n value: 'PARTICIPATED',\n label: <ParticipationBadge participation=\"PARTICIPATED\" />,\n searchText: 'Participou',\n },\n {\n value: 'NOT_PARTICIPATED',\n label: <ParticipationBadge participation=\"NOT_PARTICIPATED\" />,\n searchText: 'Não participou',\n },\n ],\n },\n render: (_value, row) => (\n <ParticipationBadge participation={participationOf(row)} />\n ),\n};\n\nconst PERFORMANCE_COLUMN: ColumnConfig<EnemMomentStudentRow> = {\n key: 'performance',\n label: 'Desempenho',\n filter: {\n paramKey: 'performances',\n multiple: true,\n allLabel: 'Todos',\n options: ENEM_MOMENT_PERFORMANCE_ORDER.map((performance) => ({\n value: performance,\n label: <EnemMomentPerformanceBadge performance={performance} />,\n searchText: ENEM_MOMENT_PERFORMANCE_LABELS[performance],\n })),\n },\n // Started, with no score yet: in no tier — the same \"—\" as the Nota column.\n render: (_value, row) =>\n row.performance === null ? (\n MISSING_VALUE\n ) : (\n <EnemMomentPerformanceBadge performance={row.performance} />\n ),\n};\n\n/** Whether the student took any exam of the cut — only then is there a modal. */\nexport const tookAnyExam = (row: EnemMomentStudentRow) =>\n row.participatedExams > 0;\n\n/** An answer count — \"—\" for a student who took nothing, not a zero. */\nconst count = (row: EnemMomentStudentRow, value: number) =>\n row.participatedExams === 0 ? MISSING_VALUE : formatCount(value);\n\n/**\n * The columns the API orders by, by the table's key — the name is `name`\n * there. Every column but Turma and the last one.\n */\nconst ORDER_BY: Readonly<Record<string, EnemMomentStudentsOrderBy>> = {\n studentName: 'name',\n totalElapsedSeconds: 'totalElapsedSeconds',\n correct: 'correct',\n incorrect: 'incorrect',\n blank: 'blank',\n hitRate: 'hitRate',\n averageScore: 'averageScore',\n};\n\nconst PARTICIPATION_FILTERS: ReadonlySet<string> =\n new Set<EnemMomentParticipationFilter>(['PARTICIPATED', 'NOT_PARTICIPATED']);\n\n/**\n * Columns of the table. The Turma filter offers the school's classes; until\n * they arrive there is nothing to offer, and it waits.\n */\nexport function createStudentColumns(\n classes: EnemMomentFilterOptions['classes']\n): ColumnConfig<EnemMomentStudentRow>[] {\n return [\n {\n key: 'studentName',\n label: 'Nome',\n className: 'min-w-[220px] max-w-[320px]',\n render: (_value, row) => (\n <div className=\"flex items-center gap-2 min-w-0\">\n <UserIcon size={24} className=\"shrink-0\" />\n <Text size=\"sm\" className=\"text-text-950 truncate\">\n {row.studentName}\n </Text>\n </div>\n ),\n },\n {\n key: 'className',\n label: 'Turma',\n filter: listFilter(\n 'classIds',\n 'Todas as turmas',\n 'Buscar turma...',\n classes.map((option) => ({\n value: option.classId,\n label: classOptionLabel(option),\n }))\n ),\n render: (_value, row) => row.className ?? MISSING_VALUE,\n },\n {\n key: 'totalElapsedSeconds',\n label: 'Tempo',\n render: (_value, row) => formatClock(row.totalElapsedSeconds),\n },\n {\n key: 'correct',\n label: 'Corretas',\n render: (_value, row) => count(row, row.correct),\n },\n {\n key: 'incorrect',\n label: 'Incorretas',\n render: (_value, row) => count(row, row.incorrect),\n },\n {\n key: 'blank',\n label: 'Em branco',\n render: (_value, row) => count(row, row.blank),\n },\n {\n key: 'hitRate',\n label: 'Taxa de acerto',\n className: 'min-w-[160px]',\n render: (_value, row) =>\n row.hitRate === null ? MISSING_VALUE : <RateCell rate={row.hitRate} />,\n },\n {\n key: 'averageScore',\n label: 'Nota',\n render: (_value, row) =>\n row.averageScore === null\n ? MISSING_VALUE\n : formatScore(row.averageScore),\n },\n // Both, in this order: the tier summarises the score beside it, and the\n // participation is what explains an entirely empty row. Only one used to\n // show, and which one depended on where the manager had come in from.\n PERFORMANCE_COLUMN,\n PARTICIPATION_COLUMN,\n ];\n}\n\n/** The tiers picked in the Desempenho filter — none is every one. */\nconst pickedPerformances = (value: unknown) =>\n Array.isArray(value) && value.length > 0\n ? (value as EnemMomentPerformance[])\n : undefined;\n\n/**\n * What the table asks the API for: its page, search, order and filters — the\n * app adds the cut and fetches.\n */\nexport type EnemMomentStudentsTableQuery = Pick<\n EnemMomentStudentsQuery,\n | 'page'\n | 'limit'\n | 'search'\n | 'classIds'\n | 'participation'\n | 'performances'\n | 'orderBy'\n | 'order'\n>;\n\n/**\n * What the table is filtering by, without the page it is on: the search, the\n * column filters and the order. The PDF of \"Baixar tabela\" asks for every\n * page under these.\n */\nexport type EnemMomentStudentsTableFilters = Omit<\n EnemMomentStudentsTableQuery,\n 'page' | 'limit'\n>;\n\n/** The filters of a table query — the query without its page. */\nexport function toStudentsTableFilters(\n query: EnemMomentStudentsTableQuery\n): EnemMomentStudentsTableFilters {\n return {\n search: query.search,\n classIds: query.classIds,\n participation: query.participation,\n performances: query.performances,\n orderBy: query.orderBy,\n order: query.order,\n };\n}\n\n/** The table's params as the students list reads them. */\nexport function toStudentsTableQuery(\n params: TableParams\n): EnemMomentStudentsTableQuery {\n const orderBy = params.sortBy ? ORDER_BY[params.sortBy] : undefined;\n const classId = pickedValue(params.classIds);\n const participation = pickedValue(params.participation);\n return {\n page: params.page,\n limit: params.limit,\n search: params.search || undefined,\n classIds: classId ? [classId] : undefined,\n participation:\n participation && PARTICIPATION_FILTERS.has(participation)\n ? (participation as EnemMomentParticipationFilter)\n : undefined,\n performances: pickedPerformances(params.performances),\n orderBy,\n order: orderBy ? params.sortOrder : undefined,\n };\n}\n\n/**\n * Whether the table has already finished one request. A remount (the app keys\n * the table on the cut) starts it over: the rows in memory are then another\n * cut's.\n */\nfunction useHasSettled(loading: boolean): boolean {\n const wasLoadingRef = useRef(false);\n const [hasSettled, setHasSettled] = useState(false);\n\n useEffect(() => {\n if (wasLoadingRef.current && !loading) setHasSettled(true);\n wasLoadingRef.current = loading;\n }, [loading]);\n\n return hasSettled;\n}\n\n/**\n * \"Desempenho por estudante\": every student the exams were sent to — how they\n * did, or that they did not take it — paged, searched, ordered and filtered\n * by the API. A student who took the exam opens their modal; one who took\n * nothing has nothing to open.\n *\n * The app fetches: the table reports what it asks for through `onQueryChange`\n * and draws the rows it is handed. Key it on the cut, so a new tab or pick\n * starts it over from page 1. While a page loads the previous rows stay on\n * screen, dimmed: swapping them for a skeleton would collapse the page and\n * throw the scroll back to the top.\n *\n * It always ends in both status columns — Desempenho and Participação. It used\n * to end in one, and which one depended on where the manager had come in from:\n * a school's page showed the participation, the unit report showed the tier.\n * Same query and same endpoint, different columns.\n *\n * With `loadExport` the header offers \"Baixar tabela\", left of the search:\n * the spreadsheet carries the whole list of the caller's students, whatever\n * the table is showing. With `loadFilteredStudents` too, the PDF prints only\n * the students the table lists under its current filters — every page, in\n * its order.\n */\nexport function StudentsTableSection({\n rows,\n pagination,\n loading,\n error,\n classes,\n onQueryChange,\n onStudentClick,\n tableId = 'enemMomentStudents',\n loadExport,\n loadFilteredStudents,\n}: Readonly<{\n rows: EnemMomentStudentRow[];\n pagination: EnemMomentPagination | null;\n loading: boolean;\n error: string | null;\n /** The Turma filter's options; empty while they load. */\n classes: EnemMomentFilterOptions['classes'];\n onQueryChange: (query: EnemMomentStudentsTableQuery) => void;\n onStudentClick?: (student: EnemMomentStudentRow) => void;\n /** Namespaces the table's params in the URL. */\n tableId?: string;\n /** Fetches the whole list for \"Baixar tabela\"; without it, no button. */\n loadExport?: () => Promise<EnemMomentStudentsExport>;\n /**\n * Every student the table lists under these filters, all pages, in its\n * order — the app adds the cut, as for `onQueryChange`. Narrows the PDF;\n * without it the PDF carries the whole list, like the spreadsheet.\n */\n loadFilteredStudents?: (\n filters: EnemMomentStudentsTableFilters\n ) => Promise<EnemMomentStudentRow[]>;\n}>) {\n const columns = useMemo(() => createStudentColumns(classes), [classes]);\n\n const hasSettled = useHasSettled(loading);\n const showSkeleton = loading && (!hasSettled || rows.length === 0);\n const isRefreshing = loading && !showSkeleton;\n\n const handleRowClick = useCallback(\n (row: EnemMomentStudentRow) => onStudentClick?.(row),\n [onStudentClick]\n );\n\n // Read when the PDF is asked for, not rendered: a ref, so the download\n // prints what the table is filtering at that moment.\n const filtersRef = useRef<EnemMomentStudentsTableFilters>({});\n\n const handleParamsChange = useCallback(\n (params: TableParams) => {\n const query = toStudentsTableQuery(params);\n filtersRef.current = toStudentsTableFilters(query);\n onQueryChange(query);\n },\n [onQueryChange]\n );\n\n const loadPdfStudents = useCallback(\n () => loadFilteredStudents?.(filtersRef.current) ?? Promise.resolve([]),\n [loadFilteredStudents]\n );\n\n const title = (\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" className=\"text-text-950\">\n Desempenho por estudante\n </Text>\n );\n\n // The provider's own header has no room for an action by the search, so\n // the download brings its own row: title left, button and search right.\n const renderHeader = (components: TableComponents): ReactNode =>\n loadExport ? (\n <div className=\"flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-4\">\n {title}\n <div className=\"flex flex-wrap items-center justify-end gap-4\">\n <StudentsTableDownload\n loadExport={loadExport}\n loadPdfStudents={loadFilteredStudents ? loadPdfStudents : undefined}\n />\n <div className=\"flex-1 lg:flex-none lg:w-[488px] print:hidden\">\n {components.search}\n </div>\n </div>\n </div>\n ) : (\n components.controls\n );\n\n const renderLayout = (components: TableComponents): ReactNode => (\n <div className=\"bg-background border border-border-50 rounded-xl p-6 space-y-4\">\n {renderHeader(components)}\n <div\n aria-busy={isRefreshing}\n className={cn(\n 'transition-opacity',\n isRefreshing && 'opacity-50 pointer-events-none'\n )}\n >\n {components.table}\n </div>\n {components.pagination && (\n <div data-print-hide>{components.pagination}</div>\n )}\n </div>\n );\n\n return (\n <SectionContent loading={false} error={error} minHeight=\"min-h-[200px]\">\n <TableProvider<EnemMomentStudentRow>\n tableId={tableId}\n data={rows}\n headers={columns}\n loading={showSkeleton}\n variant=\"borderless\"\n enableSearch\n enableTableSort\n sortMode=\"server\"\n sortableColumns={Object.keys(ORDER_BY)}\n defaultSort={{ sortBy: 'studentName', sortOrder: 'asc' }}\n enablePagination\n enableRowClick={Boolean(onStudentClick)}\n onRowClick={handleRowClick}\n rowKey=\"userInstitutionId\"\n searchPlaceholder=\"Buscar\"\n searchContainerClassName=\"print:hidden\"\n headerContent={title}\n paginationConfig={{\n itemLabel: 'estudantes',\n itemsPerPageOptions: [10, 20, 50, 100],\n defaultItemsPerPage: 10,\n totalItems: pagination?.total ?? 0,\n totalPages: pagination?.totalPages ?? 0,\n }}\n onParamsChange={handleParamsChange}\n // A student who took nothing has nothing to open.\n isRowClickable={tookAnyExam}\n emptyState={{\n title: 'Nenhum estudante encontrado',\n description:\n 'Não há estudantes para o recorte e os filtros aplicados. Ajuste a busca ou os filtros.',\n }}\n >\n {renderLayout}\n </TableProvider>\n </SectionContent>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useReactToPrint } from 'react-to-print';\nimport { DownloadSimpleIcon } from '@phosphor-icons/react/dist/csr/DownloadSimple';\nimport Button from '../Button/Button';\nimport DownloadModal from '../DownloadModal/DownloadModal';\nimport { downloadExcel, type SheetConfig } from '../../utils/exportExcel';\nimport {\n formatDateForFileName,\n formatTimestamp,\n} from '../../utils/exportFormat';\nimport type {\n EnemMomentExamStatus,\n EnemMomentStudentRow,\n EnemMomentStudentsExport,\n} from './types';\nimport { MISSING_VALUE } from './utils';\n\n/** How a status reads in the download. */\nexport const ENEM_MOMENT_EXAM_STATUS_LABELS: Readonly<\n Record<EnemMomentExamStatus, string>\n> = {\n DONE: 'Fez',\n PARTIAL: 'Parcial',\n NOT_DONE: 'Não fez',\n};\n\n/** What each status means, printed under the PDF's table. */\nconst STATUS_LEGEND = [\n 'Fez: respondeu todas as questões',\n 'Parcial: deixou questões em branco ou não entregou a prova',\n 'Não fez: não abriu a prova',\n].join(' · ');\n\nconst DOWNLOAD_ERROR = 'Não foi possível baixar a tabela. Tente novamente.';\n\nconst NOTHING_LISTED = 'Nenhum estudante corresponde aos filtros da tabela.';\n\nconst NOBODY = 'Não há estudantes para baixar.';\n\n/** A student the table lists — all the PDF needs to pick them out. */\ntype ListedStudent = Pick<EnemMomentStudentRow, 'userInstitutionId'>;\n\n/**\n * The columns of the download: the student, then one status per exam — as\n * many as the flag declares, named by their order, like the tabs.\n */\nexport function studentsExportHeaders(\n data: EnemMomentStudentsExport\n): string[] {\n return [\n 'Nome',\n 'E-mail',\n 'Turma',\n ...data.exams.map((_exam, index) => `Status Momento ${index + 1}`),\n ];\n}\n\n/**\n * One row per student, the statuses in the order of `exams`. An exam the\n * student carries no status for reads \"Não fez\": nothing of theirs was found.\n */\nexport function studentsExportRows(data: EnemMomentStudentsExport): string[][] {\n return data.students.map((student) => {\n const statusByExam = new Map(\n student.moments.map((moment) => [moment.examId, moment.status])\n );\n return [\n student.studentName,\n student.email,\n student.className ?? MISSING_VALUE,\n ...data.exams.map(\n (exam) =>\n ENEM_MOMENT_EXAM_STATUS_LABELS[\n statusByExam.get(exam.examId) ?? 'NOT_DONE'\n ]\n ),\n ];\n });\n}\n\n/**\n * The whole list cut down to the students the table lists, in the table's\n * order. A listed student missing from the list is left out: there is no\n * e-mail or status to print for them.\n */\nexport function narrowStudentsExport(\n data: EnemMomentStudentsExport,\n listed: readonly ListedStudent[]\n): EnemMomentStudentsExport {\n const byId = new Map(\n data.students.map((student) => [student.userInstitutionId, student])\n );\n return {\n exams: data.exams,\n students: listed.flatMap((row) => byId.get(row.userInstitutionId) ?? []),\n };\n}\n\n/** The download as a spreadsheet sheet. */\nexport function studentsExportSheet(\n data: EnemMomentStudentsExport\n): SheetConfig {\n return {\n name: 'Estudantes',\n headers: studentsExportHeaders(data),\n rows: studentsExportRows(data),\n };\n}\n\n/** The file's name, stamped with the day it was downloaded. */\nexport const studentsExportFileName = (date: Date = new Date()) =>\n `relatorio-simulados-momento-enem-tabela-estudantes-${formatDateForFileName(date)}`;\n\nconst PAGE_STYLE = `\n @page { size: A4 portrait; margin: 12mm; }\n @media print {\n * {\n -webkit-print-color-adjust: exact !important;\n print-color-adjust: exact !important;\n }\n }\n`;\n\n/** The list the PDF prints: the same columns as the spreadsheet. */\nfunction StudentsExportPrintable({\n data,\n filtered,\n generatedAt,\n}: Readonly<{\n data: EnemMomentStudentsExport;\n /** Whether the list is the table's, under its filters. */\n filtered: boolean;\n generatedAt: Date;\n}>) {\n const headers = studentsExportHeaders(data);\n const rows = studentsExportRows(data);\n\n return (\n <div className=\"p-2 text-text-950 font-sans\">\n <h1 className=\"text-lg font-bold\">Simulados Momento ENEM</h1>\n <p className=\"text-sm text-text-700\">\n Desempenho por estudante\n {filtered && ' · Conforme os filtros da tabela'} · Gerado em{' '}\n {formatTimestamp(generatedAt)} · {rows.length} estudantes\n </p>\n <table className=\"mt-4 w-full border-collapse text-xs\">\n <thead>\n <tr>\n {headers.map((header) => (\n <th\n key={header}\n className=\"border border-border-200 bg-background-50 px-2 py-1 text-left font-bold\"\n >\n {header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, rowIndex) => (\n <tr\n key={data.students[rowIndex].userInstitutionId}\n className=\"break-inside-avoid\"\n >\n {row.map((cell, cellIndex) => (\n <td\n key={headers[cellIndex]}\n className=\"border border-border-200 px-2 py-1\"\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n <p className=\"mt-3 text-xs text-text-700\">{STATUS_LEGEND}</p>\n </div>\n );\n}\n\n/**\n * \"Baixar tabela\" of the students table: the students with their status in\n * each exam, as a spreadsheet or a PDF.\n *\n * The spreadsheet carries every student in the caller's scope, whatever the\n * table is showing — it can be filtered in the spreadsheet itself. The PDF,\n * given `loadPdfStudents`, carries only the students the table lists under\n * its filters, in its order; without it, the whole list too.\n *\n * The app fetches; both formats ask when the user confirms, never before, and\n * the modal holds its skeleton while the lists arrive. The PDF is the\n * browser's print of a list rendered off screen for it.\n */\nexport function StudentsTableDownload({\n loadExport,\n loadPdfStudents,\n}: Readonly<{\n /** `GET /enem-moment-report/students/export`, through the app's client. */\n loadExport: () => Promise<EnemMomentStudentsExport>;\n /** Every student the table lists under its filters, all pages, in order. */\n loadPdfStudents?: () => Promise<ListedStudent[]>;\n}>) {\n const [isOpen, setIsOpen] = useState(false);\n const [isDownloading, setIsDownloading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [printData, setPrintData] = useState<{\n data: EnemMomentStudentsExport;\n filtered: boolean;\n generatedAt: Date;\n } | null>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const printedRef = useRef<object | null>(null);\n\n const clearPrintData = useCallback(() => setPrintData(null), []);\n\n const print = useReactToPrint({\n contentRef,\n documentTitle: studentsExportFileName(),\n pageStyle: PAGE_STYLE,\n onAfterPrint: clearPrintData,\n });\n\n // The list is in the DOM only after the render that received it. Once per\n // list: `print` changes identity with its options, and the modal closing\n // re-renders this component while the list is still mounted.\n useEffect(() => {\n if (printData && printedRef.current !== printData) {\n printedRef.current = printData;\n print();\n }\n }, [printData, print]);\n\n const open = useCallback(() => {\n setError(null);\n setIsOpen(true);\n }, []);\n\n const close = useCallback(() => setIsOpen(false), []);\n\n const run = useCallback(async (task: () => Promise<void>) => {\n setIsDownloading(true);\n setError(null);\n try {\n await task();\n } catch {\n setError(DOWNLOAD_ERROR);\n } finally {\n setIsDownloading(false);\n }\n }, []);\n\n const downloadExcelFile = useCallback(\n () =>\n run(async () => {\n const data = await loadExport();\n downloadExcel(studentsExportFileName(), [studentsExportSheet(data)]);\n }),\n [run, loadExport]\n );\n\n const downloadPdf = useCallback(\n () =>\n run(async () => {\n const [data, listed] = await Promise.all([\n loadExport(),\n loadPdfStudents?.() ?? null,\n ]);\n const printed = listed ? narrowStudentsExport(data, listed) : data;\n // Nothing to print: say so in the modal rather than open an empty page.\n if (printed.students.length === 0) {\n setError(listed ? NOTHING_LISTED : NOBODY);\n return;\n }\n setPrintData({\n data: printed,\n filtered: listed !== null,\n generatedAt: new Date(),\n });\n }),\n [run, loadExport, loadPdfStudents]\n );\n\n return (\n <>\n <div data-print-hide className=\"print:hidden\">\n <Button\n variant=\"outline\"\n action=\"primary\"\n size=\"small\"\n iconLeft={<DownloadSimpleIcon size={16} />}\n onClick={open}\n data-testid=\"enem-moment-students-download-btn\"\n >\n Baixar tabela\n </Button>\n </div>\n\n <DownloadModal\n isOpen={isOpen}\n onClose={close}\n title=\"Como deseja baixar a tabela?\"\n isDownloading={isDownloading}\n error={error}\n onDownloadPdf={downloadPdf}\n onDownloadExcel={downloadExcelFile}\n asyncPdf\n />\n\n {printData && (\n <div className=\"hidden\">\n <div ref={contentRef} data-testid=\"enem-moment-students-printable\">\n <StudentsExportPrintable\n data={printData.data}\n filtered={printData.filtered}\n generatedAt={printData.generatedAt}\n />\n </div>\n </div>\n )}\n </>\n );\n}\n","import type { ColumnFilterConfig } from '../TableProvider/TableProvider';\nimport type { EnemMomentFilterOptions } from './types';\n\n/**\n * A searchable, single-choice header filter — the Município and Turma filters\n * of the Momento ENEM tables.\n */\nexport const listFilter = (\n paramKey: string,\n allLabel: string,\n searchPlaceholder: string,\n options: ReadonlyArray<{ value: string; label: string }>\n): ColumnFilterConfig => ({\n paramKey,\n allLabel,\n searchable: true,\n searchPlaceholder,\n options: options.map((option) => ({\n value: option.value,\n label: option.label,\n searchText: option.label,\n })),\n});\n\n/** A class as the Turma filter names it: \"A (3ª série)\", \"Sem turma\". */\nexport const classOptionLabel = (\n option: EnemMomentFilterOptions['classes'][number]\n) => {\n const name = option.className ?? 'Sem turma';\n return option.schoolYearName ? `${name} (${option.schoolYearName})` : name;\n};\n\n/**\n * The single value a filter reports in the table params: a string, or nothing\n * for \"all\".\n */\nexport const pickedValue = (value: unknown): string | undefined =>\n typeof value === 'string' && value !== '' ? value : undefined;\n","import { PerformanceDistributionChart } from '../PerformanceDistributionChart/PerformanceDistributionChart';\nimport type { SimulatedPerformanceCounters } from '../PerformanceDistributionChart/types';\nimport { SectionContent } from './SectionContent';\nimport type {\n EnemMomentExamData,\n EnemMomentPerformance,\n EnemMomentPerformanceBand,\n EnemMomentSectionState,\n} from './types';\n\n/**\n * The API's tiers as the chart's counters, \"Não participou\" too. A tier the\n * API left out is a tier of nobody.\n */\nexport function toPerformanceCounters(\n bands: EnemMomentPerformanceBand[]\n): SimulatedPerformanceCounters {\n const count = (tag: EnemMomentPerformance) =>\n bands.find((band) => band.tag === tag)?.count ?? 0;\n return {\n highlight: count('HIGHLIGHT'),\n aboveAverage: count('ABOVE_AVERAGE'),\n belowAverage: count('BELOW_AVERAGE'),\n attentionPoint: count('ATTENTION_POINT'),\n notParticipated: count('NO_EXAM'),\n };\n}\n\n/**\n * \"Desempenho por quantidade de estudante\" of the unit report: the students\n * of the cut per \"Desempenho\" tier, who did not take it included.\n */\nexport function PerformanceDistributionSection({\n examData,\n}: Readonly<{ examData: EnemMomentSectionState<EnemMomentExamData> }>) {\n const bands = examData.data?.studentPerformanceBands;\n\n return (\n <SectionContent\n loading={examData.loading}\n error={examData.error}\n minHeight=\"min-h-[280px]\"\n >\n <PerformanceDistributionChart\n counters={bands ? toPerformanceCounters(bands) : undefined}\n />\n </SectionContent>\n );\n}\n"]}
|