UNPKG

analytica-frontend-lib

Version:

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

1 lines 37.5 kB
{"version":3,"sources":["../src/utils/categoryDataUtils.ts","../src/utils/useDynamicStudentFetching.ts"],"sourcesContent":["import type { BaseApiClient } from '../types/api';\nimport type { CategoryConfig } from '../components/CheckBoxGroup/CheckBoxGroup';\n\n/**\n * School type for categories data\n */\nexport interface School {\n id: string;\n companyName: string;\n}\n\n/**\n * SchoolYear type for categories data\n */\nexport interface SchoolYear {\n id: string;\n name: string;\n schoolId: string;\n}\n\n/**\n * Class type for categories data\n */\nexport interface Class {\n id: string;\n name: string;\n schoolYearId: string;\n}\n\n/**\n * Student type for categories data with nested school/schoolYear/class\n */\nexport interface Student {\n id: string;\n email: string;\n name: string;\n active: boolean;\n createdAt: string;\n updatedAt: string;\n userInstitutionId: string;\n institutionId: string;\n profileId: string;\n school: {\n id: string;\n name: string;\n };\n schoolYear: {\n id: string;\n name: string;\n };\n class: {\n id: string;\n name: string;\n };\n}\n\n/**\n * Fetch students using POST /students/filters based on selected schools, schoolYears, and classes\n *\n * @param apiClient - API client instance\n * @param filters - Object with schoolIds, schoolYearIds, and classIds arrays\n * @returns Promise resolving to array of students\n *\n * @example\n * ```ts\n * const students = await fetchStudentsByFilters(apiClient, {\n * schoolIds: ['school-1'],\n * schoolYearIds: ['year-1'],\n * classIds: ['class-1']\n * });\n * ```\n */\nexport async function fetchStudentsByFilters(\n apiClient: BaseApiClient,\n filters: {\n schoolIds?: string[];\n schoolYearIds?: string[];\n classIds?: string[];\n }\n): Promise<Student[]> {\n // Only make request if at least one filter is provided\n if (\n (!filters.schoolIds || filters.schoolIds.length === 0) &&\n (!filters.schoolYearIds || filters.schoolYearIds.length === 0) &&\n (!filters.classIds || filters.classIds.length === 0)\n ) {\n return [];\n }\n\n const response = await apiClient.post<{\n message: string;\n data: {\n students: Student[];\n };\n }>('/students/filters', {\n ...(filters.schoolIds &&\n filters.schoolIds.length > 0 && {\n schoolIds: filters.schoolIds,\n }),\n ...(filters.schoolYearIds &&\n filters.schoolYearIds.length > 0 && {\n schoolYearIds: filters.schoolYearIds,\n }),\n ...(filters.classIds &&\n filters.classIds.length > 0 && {\n classIds: filters.classIds,\n }),\n });\n\n return response.data.data.students || [];\n}\n\n/**\n * Fetch classes filtered by selected schools/schoolYears via GET /classes.\n * Wire format is comma-joined strings (NOT arrays): axios serializes an array\n * param with brackets (`schoolId[]=a`) which the backend's default querystring\n * parser does not fold into `schoolId`; a comma-joined string (`schoolId=a,b`)\n * is serializer-agnostic and the backend splits it. A UUID never contains a comma.\n *\n * @param apiClient - API client instance\n * @param filters - Object with schoolIds and/or schoolYearIds arrays\n * @returns Promise resolving to array of classes\n *\n * @example\n * ```ts\n * const classes = await fetchClassesByFilters(apiClient, {\n * schoolIds: ['school-1'],\n * schoolYearIds: ['year-1'],\n * });\n * ```\n */\nexport async function fetchClassesByFilters(\n apiClient: BaseApiClient,\n filters: { schoolIds?: string[]; schoolYearIds?: string[] }\n): Promise<Class[]> {\n const hasSchools = !!filters.schoolIds && filters.schoolIds.length > 0;\n const hasYears = !!filters.schoolYearIds && filters.schoolYearIds.length > 0;\n if (!hasSchools && !hasYears) return [];\n // Paginate ALL matching pages: a regional manager selecting many schools +\n // series can have > 100 filtered classes, which a single page would truncate.\n return fetchAllPages<Class>(\n apiClient,\n '/classes',\n (data) => {\n const d = data as {\n classes: Class[];\n pagination?: { totalPages: number };\n };\n return {\n items: d.classes ?? [],\n totalPages: d.pagination?.totalPages ?? 1,\n };\n },\n {\n ...(hasSchools && { schoolId: filters.schoolIds!.join(',') }),\n ...(hasYears && { schoolYearId: filters.schoolYearIds!.join(',') }),\n }\n );\n}\n\n/**\n * Fetch every page of a paginated list endpoint and aggregate the items.\n *\n * @param apiClient - API client instance\n * @param url - Endpoint to fetch (e.g. '/school')\n * @param extract - Reads `data.data` and returns the page items and totalPages\n * @param extraParams - Extra query params merged into `{ page, limit }` on every\n * request (e.g. comma-joined `schoolId`/`schoolYearId` filters for `/classes`)\n * @returns Promise resolving to all items across every page\n */\nasync function fetchAllPages<T>(\n apiClient: BaseApiClient,\n url: string,\n extract: (data: unknown) => { items: T[]; totalPages: number },\n extraParams: Record<string, unknown> = {}\n): Promise<T[]> {\n // Fetch page 1 first to learn totalPages and get the initial items.\n const firstResponse = await apiClient.get<{ message: string; data: unknown }>(\n url,\n { params: { ...extraParams, page: 1, limit: 100 } }\n );\n const first = extract(firstResponse.data.data);\n const totalPages = first.totalPages || 1;\n const all: T[] = [...(first.items ?? [])];\n\n // Single page: return page-1 items unchanged.\n if (totalPages <= 1) {\n return all;\n }\n\n // Fetch remaining pages 2..totalPages in parallel. The browser caps\n // concurrent requests per host (~6), so firing them all is safe.\n const remainingPages = Array.from(\n { length: totalPages - 1 },\n (_, i) => i + 2\n );\n const responses = await Promise.all(\n remainingPages.map((page) =>\n apiClient.get<{ message: string; data: unknown }>(url, {\n params: { ...extraParams, page, limit: 100 },\n })\n )\n );\n\n // Concatenate in PAGE ORDER via the array index (Promise.all preserves\n // input order regardless of resolution order).\n for (const response of responses) {\n const { items } = extract(response.data.data);\n all.push(...(items ?? []));\n }\n\n return all;\n}\n\n/**\n * Load categories data from API and transform to CategoryConfig format\n *\n * @param apiClient - API client instance\n * @param existingCategories - Current categories array to check if already loaded\n * @returns Promise resolving to array of CategoryConfig\n *\n * @example\n * ```ts\n * const categories = await loadCategoriesData(apiClient, []);\n * ```\n */\nexport async function loadCategoriesData(\n apiClient: BaseApiClient,\n existingCategories: CategoryConfig[]\n): Promise<CategoryConfig[]> {\n if (existingCategories.length > 0) {\n return existingCategories;\n }\n\n const [schools, schoolYears] = await Promise.all([\n fetchAllPages<School>(apiClient, '/school', (data) => {\n const d = data as {\n schools: School[];\n pagination?: { totalPages: number };\n };\n return { items: d.schools, totalPages: d.pagination?.totalPages ?? 1 };\n }),\n fetchAllPages<SchoolYear>(apiClient, '/schoolYear', (data) => {\n const d = data as {\n schoolYears: SchoolYear[];\n pagination?: { totalPages: number };\n };\n return {\n items: d.schoolYears,\n totalPages: d.pagination?.totalPages ?? 1,\n };\n }),\n ]);\n\n const transformedCategories: CategoryConfig[] = [\n {\n key: 'escola',\n label: 'Escola',\n itens: schools.map((s) => ({ id: s.id, name: s.companyName })),\n selectedIds: [],\n },\n {\n key: 'serie',\n label: 'Série',\n dependsOn: ['escola'],\n filteredBy: [{ key: 'escola', internalField: 'schoolId' }],\n itens: schoolYears.map((sy) => ({\n id: sy.id,\n name: sy.name,\n schoolId: sy.schoolId,\n })),\n selectedIds: [],\n },\n {\n key: 'turma',\n label: 'Turma',\n dependsOn: ['serie'],\n filteredBy: [{ key: 'serie', internalField: 'schoolYearId' }],\n // Classes are loaded dynamically based on selected schools/schoolYears\n itens: [],\n selectedIds: [],\n },\n {\n key: 'students',\n label: 'Alunos',\n dependsOn: ['turma'],\n filteredBy: [\n { key: 'escola', internalField: 'escolaId' },\n { key: 'serie', internalField: 'serieId' },\n { key: 'turma', internalField: 'turmaId' },\n ],\n // Students will be loaded dynamically based on selections\n itens: [],\n selectedIds: [],\n },\n ];\n\n return transformedCategories;\n}\n\n/**\n * Format time for display (HH:mm format)\n *\n * @param date - Date object to format\n * @returns Formatted time string in HH:mm format\n *\n * @example\n * ```ts\n * const time = formatTime(new Date('2025-01-01T14:30:00'));\n * // Returns: '14:30'\n * ```\n */\nexport function formatTime(date: Date): string {\n const hours = date.getHours().toString().padStart(2, '0');\n const minutes = date.getMinutes().toString().padStart(2, '0');\n return `${hours}:${minutes}`;\n}\n","import { useCallback, useRef } from 'react';\nimport type { Dispatch, SetStateAction } from 'react';\nimport type { CategoryConfig } from '../components/CheckBoxGroup/CheckBoxGroup';\nimport type { BaseApiClient } from '../types/api';\nimport {\n fetchStudentsByFilters,\n fetchClassesByFilters as defaultFetchClassesByFilters,\n} from './categoryDataUtils';\nimport type { Class } from './categoryDataUtils';\nimport type {\n FetchStudentsByFiltersFunction,\n StudentWithNestedData,\n} from './studentTypes';\n\n/**\n * Function type for fetching classes by filters\n */\nexport type FetchClassesByFiltersFunction = (\n apiClient: BaseApiClient,\n filters: { schoolIds?: string[]; schoolYearIds?: string[] }\n) => Promise<Class[]>;\n\nexport interface UseDynamicStudentFetchingOptions {\n apiClient?: BaseApiClient;\n fetchStudentsByFilters?: FetchStudentsByFiltersFunction;\n fetchClassesByFilters?: FetchClassesByFiltersFunction;\n onError?: (error: Error) => void;\n}\n\n/**\n * Extract selected IDs from categories\n */\nfunction extractSelectedIds(updatedCategories: CategoryConfig[]): {\n schoolIds: string[];\n schoolYearIds: string[];\n classIds: string[];\n studentsCategory: CategoryConfig | undefined;\n} {\n const escolaCategory = updatedCategories.find((c) => c.key === 'escola');\n const serieCategory = updatedCategories.find((c) => c.key === 'serie');\n const turmaCategory = updatedCategories.find((c) => c.key === 'turma');\n const studentsCategory = updatedCategories.find((c) => c.key === 'students');\n\n return {\n schoolIds: escolaCategory?.selectedIds || [],\n schoolYearIds: serieCategory?.selectedIds || [],\n classIds: turmaCategory?.selectedIds || [],\n studentsCategory,\n };\n}\n\n/**\n * Detect if selections have changed compared to previous selections\n */\nfunction detectSelectionChanges(\n previousSelections: {\n schoolIds: string[];\n schoolYearIds: string[];\n classIds: string[];\n },\n currentSelections: {\n schoolIds: string[];\n schoolYearIds: string[];\n classIds: string[];\n }\n): {\n schoolIdsChanged: boolean;\n schoolYearIdsChanged: boolean;\n classIdsChanged: boolean;\n shouldFetchStudents: boolean;\n} {\n const isInitialState =\n previousSelections.schoolIds.length === 0 &&\n previousSelections.schoolYearIds.length === 0 &&\n previousSelections.classIds.length === 0;\n\n const arraysEqual = (a: string[], b: string[]) => {\n if (a.length !== b.length) return false;\n return a.every((id) => b.includes(id)) && b.every((id) => a.includes(id));\n };\n\n const schoolIdsChanged =\n isInitialState ||\n !arraysEqual(previousSelections.schoolIds, currentSelections.schoolIds);\n\n const schoolYearIdsChanged =\n isInitialState ||\n !arraysEqual(\n previousSelections.schoolYearIds,\n currentSelections.schoolYearIds\n );\n\n const classIdsChanged =\n isInitialState ||\n !arraysEqual(previousSelections.classIds, currentSelections.classIds);\n\n const shouldFetchStudents =\n schoolIdsChanged || schoolYearIdsChanged || classIdsChanged;\n\n return {\n schoolIdsChanged,\n schoolYearIdsChanged,\n classIdsChanged,\n shouldFetchStudents,\n };\n}\n\n/**\n * Transform students to items format with nested data\n */\nfunction transformStudentsToItems(students: StudentWithNestedData[]): Array<{\n id: string;\n name: string;\n classId: string;\n schoolId: string;\n schoolYearId: string;\n studentId: string;\n userInstitutionId: string;\n escolaId: string;\n serieId: string;\n turmaId: string;\n}> {\n return students.map((s) => ({\n id: `${s.userInstitutionId}-${s.class.id}`,\n name: s.name,\n classId: String(s.class.id),\n schoolId: String(s.school.id),\n schoolYearId: String(s.schoolYear.id),\n studentId: s.id,\n userInstitutionId: s.userInstitutionId,\n escolaId: String(s.school.id),\n serieId: String(s.schoolYear.id),\n turmaId: String(s.class.id),\n }));\n}\n\n/**\n * Transform classes to items format.\n *\n * Each item MUST carry `schoolYearId`: the turma category's `filteredBy` is\n * `[{ key: 'serie', internalField: 'schoolYearId' }]`, so items without it are\n * filtered out and never render.\n */\nfunction transformClassesToItems(classes: Class[]): Array<{\n id: string;\n name: string;\n schoolYearId: string;\n}> {\n return classes.map((c) => ({\n id: c.id,\n name: c.name,\n schoolYearId: c.schoolYearId,\n }));\n}\n\n/**\n * Immutably replace the `itens` of a single category (by key), leaving every\n * other category untouched. Clearing a category is `setCategoryItems(cats, key, [])`.\n */\nfunction setCategoryItems(\n categories: CategoryConfig[],\n key: string,\n itens: NonNullable<CategoryConfig['itens']>\n): CategoryConfig[] {\n return categories.map((cat) => (cat.key === key ? { ...cat, itens } : cat));\n}\n\n/**\n * Commit the new `selectedIds` from a change while preserving each category's\n * prior `itens`. The `itens` for turma/alunos are owned exclusively by the async\n * write-backs, so the eager commit must never overwrite them (which would revert\n * async-loaded options when a rapid second call carries a stale snapshot).\n */\nfunction commitSelectionsPreservingItems(\n updatedCategories: CategoryConfig[],\n prev: CategoryConfig[]\n): CategoryConfig[] {\n return updatedCategories.map((cat) => {\n const prior = prev.find((p) => p.key === cat.key);\n return prior ? { ...cat, itens: prior.itens } : cat;\n });\n}\n\n/**\n * Hook to dynamically load BOTH turmas and alunos based on category selections.\n *\n * This hook provides a `handleCategoriesChange` function that:\n * - Detects changes in school/series/class selections\n * - Fetches turmas (classes) when the school/série selection changes and a série\n * is selected; clears turmas when the série selection becomes empty\n * - Fetches alunos (students) only when school/series/class selections change and\n * a turma is selected (not when only students are toggled); clears alunos otherwise\n * - Writes each fetched set back into ONLY its own category via functional state\n * updates, so a late turma response never clobbers a student write and vice-versa\n * - Transforms fetched turmas/alunos into the correct format for CategoryConfig\n * (turma items carry `schoolYearId` so the turma `filteredBy` does not hide them)\n * - Uses separate request-id guards for turmas and alunos to drop stale responses\n * - Handles errors gracefully\n *\n * @param setCategories - React state setter for categories (supports functional updates)\n * @param options - Configuration options\n * @returns Object with handleCategoriesChange function\n */\nexport function useDynamicStudentFetching(\n setCategories: Dispatch<SetStateAction<CategoryConfig[]>>,\n options: UseDynamicStudentFetchingOptions = {}\n): {\n handleCategoriesChange: (\n updatedCategories: CategoryConfig[]\n ) => Promise<void>;\n} {\n const {\n apiClient,\n fetchStudentsByFilters: customFetchStudents,\n fetchClassesByFilters: fetchClasses = defaultFetchClassesByFilters,\n onError,\n } = options;\n\n // Refs to track previous selections and prevent unnecessary API calls\n const previousSelectionsRef = useRef<{\n schoolIds: string[];\n schoolYearIds: string[];\n classIds: string[];\n }>({\n schoolIds: [],\n schoolYearIds: [],\n classIds: [],\n });\n\n // Refs to track fetch request IDs and prevent race conditions.\n // Separate ids for turmas and alunos so the two async writes never invalidate\n // each other.\n const fetchRequestId = useRef(0);\n const fetchClassesRequestId = useRef(0);\n\n /**\n * Build filters object for student fetching\n */\n const buildStudentFilters = useCallback(\n (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[],\n selectedClassIds: string[]\n ) => ({\n schoolIds: selectedSchoolIds.length > 0 ? selectedSchoolIds : undefined,\n schoolYearIds:\n selectedSchoolYearIds.length > 0 ? selectedSchoolYearIds : undefined,\n classIds: selectedClassIds.length > 0 ? selectedClassIds : undefined,\n }),\n []\n );\n\n /**\n * Fetch students using custom function\n */\n const fetchWithCustomFunction = useCallback(\n async (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[],\n selectedClassIds: string[],\n localRequestId: number\n ): Promise<StudentWithNestedData[] | null> => {\n if (!customFetchStudents) {\n return null;\n }\n\n const filters = buildStudentFilters(\n selectedSchoolIds,\n selectedSchoolYearIds,\n selectedClassIds\n );\n const students = await customFetchStudents(filters);\n\n if (localRequestId !== fetchRequestId.current) {\n return null;\n }\n\n return students;\n },\n [customFetchStudents, buildStudentFilters]\n );\n\n /**\n * Fetch students using API client\n */\n const fetchWithApiClient = useCallback(\n async (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[],\n selectedClassIds: string[],\n localRequestId: number\n ): Promise<StudentWithNestedData[] | null> => {\n if (!apiClient) {\n return null;\n }\n\n const filters = buildStudentFilters(\n selectedSchoolIds,\n selectedSchoolYearIds,\n selectedClassIds\n );\n const students = await fetchStudentsByFilters(apiClient, filters);\n\n if (localRequestId !== fetchRequestId.current) {\n return null;\n }\n\n return students;\n },\n [apiClient, buildStudentFilters]\n );\n\n /**\n * Fetch students using the appropriate method (custom function or API client)\n */\n const performStudentFetch = useCallback(\n async (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[],\n selectedClassIds: string[],\n localRequestId: number\n ): Promise<StudentWithNestedData[] | null> => {\n const students = await fetchWithCustomFunction(\n selectedSchoolIds,\n selectedSchoolYearIds,\n selectedClassIds,\n localRequestId\n );\n\n if (students !== null) {\n return students;\n }\n\n return fetchWithApiClient(\n selectedSchoolIds,\n selectedSchoolYearIds,\n selectedClassIds,\n localRequestId\n );\n },\n [fetchWithCustomFunction, fetchWithApiClient]\n );\n\n /**\n * Fetch classes (turmas) filtered by selected schools/schoolYears\n */\n const performClassFetch = useCallback(\n async (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[],\n localRequestId: number\n ): Promise<Class[] | null> => {\n if (!apiClient) {\n return null;\n }\n\n const filters = {\n schoolIds: selectedSchoolIds.length > 0 ? selectedSchoolIds : undefined,\n schoolYearIds:\n selectedSchoolYearIds.length > 0 ? selectedSchoolYearIds : undefined,\n };\n const classes = await fetchClasses(apiClient, filters);\n\n if (localRequestId !== fetchClassesRequestId.current) {\n return null;\n }\n\n return classes;\n },\n [apiClient, fetchClasses]\n );\n\n /**\n * Handle successful student fetch. Patches ONLY the students category on top of\n * the latest state so a concurrent turma write is not clobbered.\n */\n const handleSuccessfulFetch = useCallback(\n (students: StudentWithNestedData[], localRequestId: number): boolean => {\n if (localRequestId !== fetchRequestId.current) {\n return false;\n }\n\n const studentItems = transformStudentsToItems(students);\n setCategories((prev) => setCategoryItems(prev, 'students', studentItems));\n return true;\n },\n [setCategories]\n );\n\n /**\n * Handle error during student fetch\n */\n const handleFetchError = useCallback(\n (error: unknown, localRequestId: number): boolean => {\n console.error('Error fetching students:', error);\n if (onError && error instanceof Error) {\n onError(error);\n }\n\n if (localRequestId !== fetchRequestId.current) {\n return false;\n }\n\n setCategories((prev) => setCategoryItems(prev, 'students', []));\n return true;\n },\n [onError, setCategories]\n );\n\n /**\n * Handle successful class fetch. Patches ONLY the turma category on top of the\n * latest state so a concurrent student write is not clobbered.\n */\n const handleSuccessfulClassFetch = useCallback(\n (classes: Class[], localRequestId: number): boolean => {\n if (localRequestId !== fetchClassesRequestId.current) {\n return false;\n }\n\n const classItems = transformClassesToItems(classes);\n setCategories((prev) => setCategoryItems(prev, 'turma', classItems));\n return true;\n },\n [setCategories]\n );\n\n /**\n * Handle error during class fetch\n */\n const handleClassFetchError = useCallback(\n (error: unknown, localRequestId: number): boolean => {\n console.error('Error fetching classes:', error);\n if (onError && error instanceof Error) {\n onError(error);\n }\n\n if (localRequestId !== fetchClassesRequestId.current) {\n return false;\n }\n\n setCategories((prev) => setCategoryItems(prev, 'turma', []));\n return true;\n },\n [onError, setCategories]\n );\n\n /**\n * Fetch turmas (or clear them) in response to a school/série selection change\n */\n const handleClassesChange = useCallback(\n async (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[]\n ): Promise<void> => {\n // No apiClient => this consumer manages `turma` itself (e.g. AlertsManager\n // with pre-populated categories). Never fetch OR clear turma in that case.\n if (!apiClient) return;\n\n // Turma depends on a selected série: fetch only when a série is selected.\n if (selectedSchoolYearIds.length > 0) {\n fetchClassesRequestId.current += 1;\n const localRequestId = fetchClassesRequestId.current;\n\n try {\n const classes = await performClassFetch(\n selectedSchoolIds,\n selectedSchoolYearIds,\n localRequestId\n );\n\n if (classes === null) {\n return; // Request was invalidated\n }\n\n handleSuccessfulClassFetch(classes, localRequestId);\n } catch (error) {\n handleClassFetchError(error, localRequestId);\n }\n } else {\n // Série selection became empty -> reset turma options\n fetchClassesRequestId.current += 1;\n setCategories((prev) => setCategoryItems(prev, 'turma', []));\n }\n },\n [\n apiClient,\n performClassFetch,\n handleSuccessfulClassFetch,\n handleClassFetchError,\n setCategories,\n ]\n );\n\n /**\n * Fetch alunos (or clear them) in response to a selection change\n */\n const handleStudentsChange = useCallback(\n async (\n selectedSchoolIds: string[],\n selectedSchoolYearIds: string[],\n selectedClassIds: string[],\n studentsCategory: CategoryConfig | undefined\n ): Promise<void> => {\n if (selectedClassIds.length > 0 && studentsCategory) {\n // Increment request ID to invalidate any in-flight requests\n fetchRequestId.current += 1;\n const localRequestId = fetchRequestId.current;\n\n try {\n const students = await performStudentFetch(\n selectedSchoolIds,\n selectedSchoolYearIds,\n selectedClassIds,\n localRequestId\n );\n\n if (students === null) {\n return; // Request was invalidated\n }\n\n handleSuccessfulFetch(students, localRequestId);\n } catch (error) {\n handleFetchError(error, localRequestId);\n }\n } else if (studentsCategory) {\n // If no classes selected after a change, clear students\n fetchRequestId.current += 1;\n setCategories((prev) => setCategoryItems(prev, 'students', []));\n }\n },\n [\n performStudentFetch,\n handleSuccessfulFetch,\n handleFetchError,\n setCategories,\n ]\n );\n\n const handleCategoriesChange = useCallback(\n async (updatedCategories: CategoryConfig[]) => {\n // If no fetch function is provided, just update categories\n if (!customFetchStudents && !apiClient) {\n setCategories(updatedCategories);\n return;\n }\n\n // Extract selected IDs from categories\n const {\n schoolIds: selectedSchoolIds,\n schoolYearIds: selectedSchoolYearIds,\n classIds: selectedClassIds,\n studentsCategory,\n } = extractSelectedIds(updatedCategories);\n\n // Detect if selections have changed\n const previousSelections = previousSelectionsRef.current;\n const { schoolIdsChanged, schoolYearIdsChanged, shouldFetchStudents } =\n detectSelectionChanges(previousSelections, {\n schoolIds: selectedSchoolIds,\n schoolYearIds: selectedSchoolYearIds,\n classIds: selectedClassIds,\n });\n\n // Update previous selections\n previousSelectionsRef.current = {\n schoolIds: [...selectedSchoolIds],\n schoolYearIds: [...selectedSchoolYearIds],\n classIds: [...selectedClassIds],\n };\n\n // Commit the new selections immediately, but NEVER the snapshot's `itens`\n // (owned exclusively by the async write-backs below). This keeps the\n // no-clobber invariant regardless of caller timing.\n setCategories((prev) =>\n commitSelectionsPreservingItems(updatedCategories, prev)\n );\n\n // Dynamic TURMA fetching: react to school/série selection changes\n if (schoolIdsChanged || schoolYearIdsChanged) {\n await handleClassesChange(selectedSchoolIds, selectedSchoolYearIds);\n }\n\n // Dynamic ALUNOS fetching: react to school/série/turma selection changes\n // (skipped when only student selections changed)\n if (shouldFetchStudents) {\n await handleStudentsChange(\n selectedSchoolIds,\n selectedSchoolYearIds,\n selectedClassIds,\n studentsCategory\n );\n }\n },\n [\n apiClient,\n customFetchStudents,\n setCategories,\n handleClassesChange,\n handleStudentsChange,\n ]\n );\n\n return {\n handleCategoriesChange,\n };\n}\n"],"mappings":"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","names":[]}