UNPKG

analytica-frontend-lib

Version:

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

1 lines 1.38 MB
{"version":3,"sources":["../src/components/CheckBoxGroup/CheckBoxGroup.tsx","../src/components/AlertManager/AlertsManager.tsx","../src/components/AlertManager/AlertSteps/MessageStep.tsx","../src/components/AlertManager/AlertSteps/RecipientsStep.tsx","../src/components/AlertManager/AlertSteps/DateStep.tsx","../src/components/AlertManager/AlertSteps/PreviewStep.tsx","../src/components/AlertManager/validation.ts","../src/components/shared/SendModalBase/utils/applyChainedAutoSelection.ts","../src/components/shared/SendModalBase/hooks/useDateTimeHandlers.ts","../src/components/shared/SendModalBase/hooks/useCategoryInitialization.ts","../src/components/shared/SendModalBase/hooks/useCategorySync.ts","../src/components/shared/SendModalBase/components/SendModalError.tsx","../src/components/shared/SendModalBase/components/RecipientStep.tsx","../src/components/DateTimeInput/DateTimeInput.tsx","../src/components/shared/SendModalBase/components/DeadlineStep.tsx","../src/components/shared/SendModalBase/components/SendModalFooter.tsx","../src/hooks/useActivityFiltersData.ts","../src/components/ActivityCreate/ActivityCreate.tsx","../src/hooks/useScreen.ts","../src/components/ActivityCreate/ActivityCreate.utils.ts","../src/components/ActivityCreate/components/ActivityCreateSkeleton.tsx","../src/components/ActivityCreate/components/ActivityCreateHeader.tsx","../src/components/ActivityListQuestions/ActivityListQuestions.tsx","../src/utils/questionFiltersConverter.ts","../src/assets/icons/Activities.tsx","../src/components/ActivityCreate/components/ActivityCreateContent.tsx","../src/components/RecommendedLessonCreate/RecommendedLessonCreate.tsx","../src/components/LessonFilters/LessonFilters.tsx","../src/utils/lessonFilters.ts","../src/components/ActivityFilters/components/SubjectsFilter.tsx","../src/components/ActivityFilters/components/KnowledgeStructureFilter.tsx","../src/components/ActivityFilters/components/FilterActions.tsx","../src/components/ActivityFilters/utils/useInitialFiltersLoader.ts","../src/components/ActivityFilters/utils/useKnowledgeStructureInitialFilters.ts","../src/components/LessonBank/LessonBank.tsx","../src/components/LessonBank/hooks/useLessonBank.ts","../src/assets/icons/subjects/Video.tsx","../src/components/shared/LessonWatchModal/LessonWatchModal.tsx","../src/components/shared/ToastNotification/ToastNotification.tsx","../src/components/shared/ToastNotification/useToastNotification.ts","../src/components/LessonPreview/LessonPreview.tsx","../src/components/LessonPreview/components/AddActivityOptionModal.tsx","../src/components/ChooseActivityModelModal/ChooseActivityModelModal.tsx","../src/components/ChooseActivityModelModal/components/ActivityModelsList.tsx","../src/components/shared/ModelsTabBase/createModelsTableColumnsBase.tsx","../src/components/ActivityCardQuestionPreview/ActivityCardQuestionPreview.tsx","../src/components/ChooseActivityModelModal/components/ActivityModelDetails.tsx","../src/store/lessonFiltersStore.ts","../src/components/RecommendedLessonCreate/RecommendedLessonCreate.utils.ts","../src/components/RecommendedLessonCreate/components/RecommendedLessonCreateSkeleton.tsx","../src/components/RecommendedLessonCreate/components/RecommendedLessonCreateHeader.tsx","../src/components/Table/ColumnFilterMenu.tsx","../src/components/Table/useColumnFilters.ts","../src/enums/FilterEnums.ts","../src/components/Filter/FilterModal.tsx","../src/components/SimulatedFilters/SimulatedFiltersModal.tsx","../src/components/SimulatedFilters/hooks.ts","../src/components/SimulatedFilters/StudentsFilterSection.tsx","../src/components/SimulatedSubjectMenu/SimulatedSubjectMenu.tsx","../src/components/SimulatedSubjectMenu/useSimulatedSubjects.ts","../src/components/SimulatedStudentRanking/SimulatedStudentRanking.tsx","../src/components/PerformanceDistributionChart/PerformanceDistributionChart.tsx","../src/components/SimulatedContentDetailsModal/SimulatedContentDetailsModal.tsx","../src/components/TableProvider/TableProvider.tsx","../src/components/SimulatedContentDetailsModal/useSimulatedContentDetails.ts","../src/components/EssayCompetencies/EssayCompetenciesTable.tsx","../src/components/EssayCompetencies/useEssayCompetenciesOverview.ts","../src/components/EssayCompetencies/EssayCompetenceDetailsModal.tsx","../src/components/EssayCompetencies/useEssayCompetenceDetails.ts","../src/components/EssayStudentDetailsModal/EssayStudentDetailsModal.tsx","../src/components/EssayStudentDetailsModal/useEssayStudentDetails.ts","../src/components/ActivityFilters/ActivityFilters.tsx","../src/store/questionFiltersStore.ts","../src/components/FillInBlanks/FillInBlanks.tsx","../src/utils/parseContent.ts","../src/components/ConnectDots/ConnectDots.tsx","../src/hooks/useAppContent.ts","../src/components/ActivityCardQuestionBanks/ActivityCardQuestionBanks.tsx","../src/components/ActivityPreview/ActivityPreview.tsx","../src/components/QuestionsPdfGenerator/QuestionsPdfGenerator.tsx","../src/components/HierarchicalCheckboxGroup/HierarchicalCheckboxGroup.tsx","../src/components/ActivityDetails/ActivityDetails.tsx","../src/hooks/useActivityDetails.ts","../src/components/SendActivityModal/SendActivityModal.tsx","../src/components/SaveActivityModelModal/SaveActivityModelModal.tsx","../src/components/SendLessonModal/SendLessonModal.tsx","../src/components/SendLessonModal/hooks/useSendLessonModal.ts","../src/components/SendLessonModal/validation.ts","../src/components/RecommendedLessonsHistory/RecommendedLessonsHistory.tsx","../src/components/RecommendedLessonsHistory/EditRecommendedLessonModal.tsx","../src/components/shared/SendModalBase/hooks/useEditModalForm.ts","../src/components/shared/SendModalBase/utils/splitDateTime.ts","../src/components/shared/ModelsTabBase/ModelsTabBase.tsx","../src/components/ActivitiesHistory/components/ErrorDisplay.tsx","../src/components/RecommendedLessonsHistory/config/modelsFiltersConfig.ts","../src/components/RecommendedLessonsHistory/utils/filterBuilders.ts","../src/components/RecommendedLessonsHistory/tabs/ModelsTab.tsx","../src/components/RecommendedLessonsHistory/config/draftsFiltersConfig.ts","../src/components/RecommendedLessonsHistory/tabs/DraftsTab.tsx","../src/hooks/useRecommendedLessonDetails.ts","../src/components/ActivitiesHistory/ActivitiesHistory.tsx","../src/components/ActivitiesHistory/tabs/HistoryTab.tsx","../src/components/ActivitiesHistory/config/historyTableColumns.tsx","../src/components/ActivitiesHistory/utils/renderTruncatedText.tsx","../src/components/ActivitiesHistory/utils/filterBuilders.ts","../src/components/ActivitiesHistory/utils/filterOptions.ts","../src/components/ActivitiesHistory/config/historyFiltersConfig.ts","../src/components/ActivitiesHistory/config/modelsFiltersConfig.ts","../src/components/ActivitiesHistory/tabs/ModelsTab.tsx","../src/components/ActivitiesHistory/tabs/DraftsTab.tsx","../src/components/BasePageLayout/BasePageLayout.tsx","../src/components/ActivityPageLayout/ActivityPageLayout.tsx","../src/components/UnifiedHistoryPage/UnifiedHistoryPage.tsx","../src/components/ExamPageLayout/ExamPageLayout.tsx","../src/components/ActivityPageLayout/activitiesTableConfig.tsx","../src/components/UnifiedHistoryPage/config.ts","../src/components/UnifiedHistoryPage/EditActivityModal.tsx","../src/components/UnifiedDraftModelPage/UnifiedDraftModelPage.tsx","../src/components/UnifiedDraftModelPage/config.ts","../src/hooks/useActivityDraftModelPage.ts","../src/components/ExamPageLayout/examDraftsModelsTableConfig.tsx","../src/utils/draftModelFilterHelpers.ts","../src/components/Chat/ChatLoading.tsx","../src/hooks/useForum.ts","../src/components/PeriodSelectorModal/PeriodSelectorModal.tsx","../src/components/PrintableUsersTable/PrintableUsersTable.tsx","../src/components/SimulatedPerformance/SimulatedPerformanceView.tsx","../src/components/SimulationsPage/SimulationsPage.tsx","../src/components/SimulationsPage/SimulationsDetailModal.tsx","../src/components/ExamPageLayout/GabaritoPreview.tsx","../src/components/ExamPageLayout/GabaritosBatchPreview.tsx","../src/components/ExamDetailsLayout/ExamDetailsHeader.tsx","../src/components/ExamDetailsLayout/ExamStatsCards.tsx","../src/components/ExamDetailsLayout/ExamStudentsTable.tsx","../src/components/ExamDetailsLayout/examStudentsTableConfig.tsx","../src/components/ExamDetailsLayout/ExamDetailsPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n AccordionGroup,\n Badge,\n CardAccordation,\n CheckBox,\n cn,\n Text,\n Divider,\n Input,\n} from '../../';\nimport { MagnifyingGlassIcon } from '@phosphor-icons/react/dist/csr/MagnifyingGlass';\nimport {\n areSelectedIdsEqual,\n isCategoryEnabled as isCategoryEnabledHelper,\n getBadgeText as getBadgeTextHelper,\n handleAccordionValueChange as handleAccordionValueChangeHelper,\n calculateFormattedItemsForAutoSelection,\n} from './CheckBoxGroup.helpers';\n\nexport type Item = {\n id: string;\n name: string;\n [key: string]: unknown;\n};\n\nexport type CategoryConfig = {\n key: string;\n label: string;\n selectedIds?: string[];\n dependsOn?: string[];\n itens?: Item[];\n filteredBy?: { key: string; internalField: string }[];\n /** When true, renders a search input to filter this category's items by name */\n searchable?: boolean;\n};\n\nexport const CheckboxGroup = ({\n categories,\n onCategoriesChange,\n compactSingleItem = true,\n showDivider = true,\n showSingleItem = false,\n disableAutoSelection = false,\n}: {\n categories: CategoryConfig[];\n onCategoriesChange: (categories: CategoryConfig[]) => void;\n compactSingleItem?: boolean;\n showDivider?: boolean;\n showSingleItem?: boolean;\n disableAutoSelection?: boolean;\n}) => {\n const [openAccordion, setOpenAccordion] = useState<string>('');\n // Search query per category key (only used by categories with searchable: true)\n const [searchQueries, setSearchQueries] = useState<Record<string, string>>(\n {}\n );\n\n const handleSearchChange = (categoryKey: string, value: string) => {\n setSearchQueries((prev) => ({ ...prev, [categoryKey]: value }));\n };\n\n // Filters a category's formatted groups by its search query (case-insensitive).\n // Returns the groups unchanged when there is no active query.\n const filterGroupsBySearch = (\n categoryKey: string,\n groups: { groupLabel?: string; itens: Item[] }[]\n ) => {\n const query = searchQueries[categoryKey]?.trim().toLowerCase();\n if (!query) return groups;\n return groups.map((group) => ({\n ...group,\n itens: group.itens.filter((item) =>\n item.name.toLowerCase().includes(query)\n ),\n }));\n };\n\n // Refs to prevent infinite loops and track auto-selection state\n const onCategoriesChangeRef = useRef(onCategoriesChange);\n const previousCategoriesRef = useRef<CategoryConfig[]>(categories);\n // Track which categories have had auto-selection applied (by stringified selectedIds)\n const appliedAutoSelectionRef = useRef<string>('');\n\n // Update ref when onCategoriesChange changes\n useEffect(() => {\n onCategoriesChangeRef.current = onCategoriesChange;\n }, [onCategoriesChange]);\n\n // Use helper function for comparing selectedIds arrays\n\n // Auto-seleciona categorias com apenas um item (considerando itens filtrados)\n const categoriesWithAutoSelection = useMemo(() => {\n // Se auto-seleção está desabilitada, retorna as categorias sem modificação\n if (disableAutoSelection) {\n return categories;\n }\n\n return categories.map((category) => {\n // Get filtered/visible items for this category\n const filteredItems = calculateFormattedItemsForAutoSelection(\n category,\n categories\n );\n\n // Se tem apenas um item filtrado/visível e nenhum está selecionado, auto-seleciona\n if (\n filteredItems.length === 1 &&\n (!category.selectedIds || category.selectedIds.length === 0)\n ) {\n return {\n ...category,\n selectedIds: [filteredItems[0].id],\n };\n }\n return category;\n });\n }, [categories, disableAutoSelection]);\n\n // Aplica a auto-seleção se necessário\n // Note: onCategoriesChange should be memoized by the parent component to prevent re-renders\n useEffect(() => {\n // Check if categories have actually changed by comparing with previous reference\n const categoriesChanged = categories !== previousCategoriesRef.current;\n previousCategoriesRef.current = categories;\n\n // Check for auto-selection changes using efficient comparison\n const hasAutoSelectionChanges = categoriesWithAutoSelection.some(\n (cat, index) => {\n const originalCat = categories[index];\n return !areSelectedIdsEqual(cat.selectedIds, originalCat.selectedIds);\n }\n );\n\n if (!hasAutoSelectionChanges) {\n // No auto-selection needed, reset tracking if categories changed\n if (categoriesChanged) {\n appliedAutoSelectionRef.current = '';\n }\n return;\n }\n\n // Create a signature of the auto-selection to prevent duplicate notifications\n const autoSelectionSignature = JSON.stringify(\n categoriesWithAutoSelection.map((c) => c.selectedIds)\n );\n\n // Only notify if this exact auto-selection hasn't been applied yet\n if (appliedAutoSelectionRef.current !== autoSelectionSignature) {\n appliedAutoSelectionRef.current = autoSelectionSignature;\n onCategoriesChangeRef.current(categoriesWithAutoSelection);\n }\n }, [categoriesWithAutoSelection, categories]);\n\n const isCheckBoxIsSelected = (categoryKey: string, itemId: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return false;\n return category.selectedIds?.includes(itemId) || false;\n };\n\n const isMinimalOneCheckBoxIsSelected = (categoryKey: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return false;\n\n // Obtém apenas os itens visíveis (dependências + busca ativa)\n const formattedItems = getDisplayItems(categoryKey);\n const filteredItems = formattedItems.flatMap((group) => group.itens || []);\n const filteredItemIds = filteredItems.map((item) => item.id);\n\n // Verifica se pelo menos um item filtrado está selecionado\n return filteredItemIds.some((itemId) =>\n category.selectedIds?.includes(itemId)\n );\n };\n\n const areAllFilteredItemsSelected = (categoryKey: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return false;\n\n // Obtém apenas os itens visíveis (dependências + busca ativa)\n const formattedItems = getDisplayItems(categoryKey);\n const filteredItems = formattedItems.flatMap((group) => group.itens || []);\n\n // Se não há itens filtrados, retorna false\n if (filteredItems.length === 0) return false;\n\n const filteredItemIds = filteredItems.map((item) => item.id);\n\n // Verifica se TODOS os itens filtrados estão selecionados\n return filteredItemIds.every((itemId) =>\n category.selectedIds?.includes(itemId)\n );\n };\n\n // Helper function to create combination of two arrays\n const createCombinations = (\n acc: string[][],\n currentArray: string[]\n ): string[][] => {\n const combinations: string[][] = [];\n for (const existingCombo of acc) {\n for (const item of currentArray) {\n combinations.push([...existingCombo, item]);\n }\n }\n return combinations;\n };\n\n // Helper function to calculate cartesian product of arrays\n const cartesian = (arr: string[][]): string[][] => {\n return arr.reduce(createCombinations, [[]] as string[][]);\n };\n\n // Helper function to get selected IDs for filters\n const getSelectedIdsForFilters = (\n filters: { key: string; internalField: string; label?: string }[]\n ) => {\n return filters.map((f) => {\n const parentCat = categories.find((c) => c.key === f.key);\n if (!parentCat?.selectedIds?.length) {\n return [];\n }\n return parentCat.selectedIds;\n });\n };\n\n // Helper function to generate group label for single filter\n const generateSingleFilterLabel = (\n filter: { key: string; internalField: string },\n comboId: string\n ) => {\n const cat = categories.find((c) => c.key === filter.key);\n return cat?.itens?.find((i) => i.id === comboId)?.name || comboId;\n };\n\n // Helper function to generate group label for multiple filters\n const generateMultipleFiltersLabel = (\n filters: { key: string; internalField: string }[],\n comboIds: string[]\n ) => {\n const firstCat = categories.find((c) => c.key === filters[0].key);\n const firstVal =\n firstCat?.itens?.find((i) => i.id === comboIds[0])?.name || comboIds[0];\n\n const labelParts: string[] = [firstVal];\n\n for (let idx = 1; idx < filters.length; idx++) {\n const f = filters[idx];\n const cat = categories.find((c) => c.key === f.key);\n const val =\n cat?.itens?.find((i) => i.id === comboIds[idx])?.name || comboIds[idx];\n labelParts.push(`(${val})`);\n }\n return labelParts.join(' ');\n };\n\n // Helper function to process combination and add to grouped map\n const processCombination = (\n comboIds: string[],\n filters: { key: string; internalField: string; label?: string }[],\n category: CategoryConfig,\n groupedMap: Record<string, { groupLabel?: string; itens: Item[] }>\n ) => {\n const filteredItems = (category?.itens || []).filter((item) =>\n filters.every((f, idx) => item[f.internalField] === comboIds[idx])\n );\n\n if (filteredItems.length === 0) return;\n\n let groupLabel: string | undefined = undefined;\n\n if (filters.length === 1) {\n groupLabel = generateSingleFilterLabel(filters[0], comboIds[0]);\n } else if (filters.length > 1) {\n groupLabel = generateMultipleFiltersLabel(filters, comboIds);\n }\n\n const key = groupLabel || '';\n if (!groupedMap[key]) {\n groupedMap[key] = groupLabel ? { groupLabel, itens: [] } : { itens: [] };\n }\n groupedMap[key].itens.push(...filteredItems);\n };\n\n // Helper function to calculate formatted items for a category\n const calculateFormattedItems = (categoryKey: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n\n if (!category?.dependsOn || category.dependsOn.length === 0) {\n return [{ itens: category?.itens || [] }];\n }\n\n // Check if category is enabled based on dependencies (inline to avoid stale closure)\n const isEnabled = category.dependsOn.every((depKey) => {\n const depCat = categories.find((c) => c.key === depKey);\n return depCat?.selectedIds && depCat.selectedIds.length > 0;\n });\n\n // If category is disabled, return empty items array to hide all items\n if (!isEnabled) {\n return [{ itens: [] }];\n }\n\n const filters =\n (category.filteredBy as {\n key: string;\n internalField: string;\n label?: string;\n }[]) || [];\n\n if (filters.length === 0) {\n return [{ itens: category?.itens || [] }];\n }\n\n const selectedIdsArr = getSelectedIdsForFilters(filters);\n\n if (selectedIdsArr.some((arr) => arr.length === 0)) {\n return [{ itens: [] }];\n }\n\n const combinations = cartesian(selectedIdsArr);\n const groupedMap: Record<string, { groupLabel?: string; itens: Item[] }> =\n {};\n\n for (const comboIds of combinations) {\n processCombination(comboIds, filters, category, groupedMap);\n }\n\n const groupedItems = Object.values(groupedMap).filter(\n (g) => g.itens.length\n );\n\n return groupedItems.length ? groupedItems : [{ itens: [] }];\n };\n\n const formattedItemsMap = useMemo(() => {\n const formattedItemsMap: Record<\n string,\n { groupLabel?: string; itens: Item[] }[]\n > = {};\n\n for (const category of categories) {\n const formattedItems = calculateFormattedItems(category.key);\n formattedItemsMap[category.key] = formattedItems;\n }\n\n return formattedItemsMap;\n }, [categories]);\n\n const getFormattedItems = (categoryKey: string) => {\n return formattedItemsMap[categoryKey] || [{ itens: [] }];\n };\n\n // Items actually visible to the user: dependency-filtered items narrowed by\n // the active search query for searchable categories. Selection state, the\n // \"select all\" toggle and the badge all operate on these so they match the\n // rendered list.\n const getDisplayItems = (categoryKey: string) => {\n const formattedItems = getFormattedItems(categoryKey);\n const category = categories.find((c) => c.key === categoryKey);\n return category?.searchable\n ? filterGroupsBySearch(categoryKey, formattedItems)\n : formattedItems;\n };\n\n // Helper function to get badge text for category\n const getBadgeText = (category: CategoryConfig): string => {\n const formattedItems = getDisplayItems(category.key);\n return getBadgeTextHelper(category, formattedItems);\n };\n\n // Helper function to check if category is enabled\n const isCategoryEnabled = (category: CategoryConfig): boolean => {\n return isCategoryEnabledHelper(category, categories);\n };\n\n // Helper function to handle accordion value change\n const handleAccordionValueChange = (value: string | string[] | undefined) => {\n const newValue = handleAccordionValueChangeHelper(\n value,\n categories,\n isCategoryEnabled\n );\n if (newValue !== null) {\n setOpenAccordion(newValue);\n }\n };\n\n const getDependentCategories = (categoryKey: string): string[] => {\n return categories\n .filter((cat) => cat.dependsOn?.includes(categoryKey))\n .map((cat) => cat.key);\n };\n\n // Helper function to find items to remove from dependent category\n const findItemsToRemove = (\n depCategory: CategoryConfig,\n relevantFilter: { key: string; internalField: string },\n deselectedItemId: string\n ): string[] => {\n return (\n depCategory.itens\n ?.filter(\n (item) => item[relevantFilter.internalField] === deselectedItemId\n )\n .map((item) => item.id) || []\n );\n };\n\n // Helper function to process dependent category for deselection\n const processDependentCategory = (\n depCategoryKey: string,\n categoryKey: string,\n deselectedItemId: string,\n itemsToDeselect: Record<string, string[]>\n ) => {\n const depCategory = categories.find((c) => c.key === depCategoryKey);\n if (!depCategory?.filteredBy) return;\n\n const relevantFilter = depCategory.filteredBy.find(\n (f) => f.key === categoryKey\n );\n if (!relevantFilter) return;\n\n const itemsToRemove = findItemsToRemove(\n depCategory,\n relevantFilter,\n deselectedItemId\n );\n if (itemsToRemove.length > 0) {\n itemsToDeselect[depCategoryKey] = itemsToRemove;\n }\n };\n\n const getItemsToDeselect = (\n categoryKey: string,\n deselectedItemId: string\n ) => {\n const deselectedItem = categories\n .find((c) => c.key === categoryKey)\n ?.itens?.find((item) => item.id === deselectedItemId);\n if (!deselectedItem) return {};\n\n const itemsToDeselect: Record<string, string[]> = {};\n const dependentCategories = getDependentCategories(categoryKey);\n\n for (const depCategoryKey of dependentCategories) {\n processDependentCategory(\n depCategoryKey,\n categoryKey,\n deselectedItemId,\n itemsToDeselect\n );\n }\n\n return itemsToDeselect;\n };\n\n // Helper function to update category with new selected IDs\n const updateCategorySelectedIds = (\n updatedCategories: CategoryConfig[],\n depCategoryIndex: number,\n depCategory: CategoryConfig,\n itemIds: string[]\n ): CategoryConfig[] => {\n const newSelectedIds =\n depCategory.selectedIds?.filter((id) => !itemIds.includes(id)) || [];\n\n updatedCategories[depCategoryIndex] = {\n ...depCategory,\n selectedIds: newSelectedIds,\n };\n\n return updatedCategories;\n };\n\n // Helper function to apply recursive cascade deselection\n const applyRecursiveCascade = (\n depCategoryKey: string,\n itemIds: string[],\n updatedCategories: CategoryConfig[]\n ): CategoryConfig[] => {\n let result = updatedCategories;\n for (const itemId of itemIds) {\n result = applyCascadeDeselection(depCategoryKey, itemId, result);\n }\n return result;\n };\n\n const applyCascadeDeselection = (\n categoryKey: string,\n deselectedItemId: string,\n currentCategories: CategoryConfig[]\n ): CategoryConfig[] => {\n const itemsToDeselect = getItemsToDeselect(categoryKey, deselectedItemId);\n let updatedCategories = [...currentCategories];\n\n for (const [depCategoryKey, itemIds] of Object.entries(itemsToDeselect)) {\n const depCategoryIndex = updatedCategories.findIndex(\n (c) => c.key === depCategoryKey\n );\n\n if (depCategoryIndex !== -1) {\n const depCategory = updatedCategories[depCategoryIndex];\n updatedCategories = updateCategorySelectedIds(\n updatedCategories,\n depCategoryIndex,\n depCategory,\n itemIds\n );\n updatedCategories = applyRecursiveCascade(\n depCategoryKey,\n itemIds,\n updatedCategories\n );\n }\n }\n\n return updatedCategories;\n };\n\n const toggleAllInCategory = (categoryKey: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return;\n\n // Obtém apenas os itens visíveis (dependências + busca ativa)\n const formattedItems = getDisplayItems(categoryKey);\n const filteredItems = formattedItems.flatMap((group) => group.itens || []);\n const filteredItemIds = filteredItems.map((item) => item.id);\n\n // Verifica quantos itens filtrados estão selecionados\n const selectedFilteredCount = filteredItemIds.filter((itemId) =>\n category.selectedIds?.includes(itemId)\n ).length;\n\n // Se NENHUM item filtrado está selecionado OU pelo menos um está selecionado mas não todos,\n // então seleciona todos os itens filtrados\n // Se TODOS os itens filtrados estão selecionados, então deseleciona todos os filtrados\n const allFilteredSelected =\n selectedFilteredCount === filteredItemIds.length;\n\n const newSelection = allFilteredSelected\n ? category.selectedIds?.filter((id) => !filteredItemIds.includes(id)) ||\n []\n : [\n ...(category.selectedIds || []),\n ...filteredItemIds.filter(\n (id) => !category.selectedIds?.includes(id)\n ),\n ];\n\n let updatedCategories = categories.map((c) =>\n c.key === categoryKey ? { ...c, selectedIds: newSelection } : c\n );\n\n // Se está deselecionando, aplica cascata para os itens que foram deselecionados\n if (allFilteredSelected) {\n for (const itemId of filteredItemIds) {\n updatedCategories = applyCascadeDeselection(\n categoryKey,\n itemId,\n updatedCategories\n );\n }\n }\n\n onCategoriesChange(updatedCategories);\n };\n\n const toggleItem = (categoryKey: string, itemId: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return;\n const isCurrentlySelected = category.selectedIds?.includes(itemId);\n const newSelection = isCurrentlySelected\n ? category.selectedIds?.filter((id) => id !== itemId)\n : [...(category.selectedIds || []), itemId];\n\n let updatedCategories = categories.map((c) =>\n c.key === categoryKey ? { ...c, selectedIds: newSelection } : c\n );\n\n // Se está deselecionando, aplica cascata\n if (isCurrentlySelected) {\n updatedCategories = applyCascadeDeselection(\n categoryKey,\n itemId,\n updatedCategories\n );\n }\n\n onCategoriesChange(updatedCategories);\n };\n\n // Helper component to render individual checkbox item\n const renderCheckboxItem = (item: Item, categoryKey: string) => {\n // Generate unique ID by combining category key and item id to avoid conflicts\n const uniqueId = `${categoryKey}-${item.id}`;\n\n return (\n <div key={item.id} className=\"flex items-center gap-3 px-2\">\n <CheckBox\n id={uniqueId}\n checked={isCheckBoxIsSelected(categoryKey, item.id)}\n onChange={() => toggleItem(categoryKey, item.id)}\n />\n <label\n htmlFor={uniqueId}\n className=\"text-sm text-text-950 cursor-pointer select-none\"\n >\n {item.name}\n </label>\n </div>\n );\n };\n\n // Helper component to render formatted group\n const renderFormattedGroup = (\n formattedGroup: { groupLabel?: string; itens: Item[] },\n idx: number,\n categoryKey: string\n ) => (\n <div\n key={formattedGroup.groupLabel || `group-${idx}`}\n className=\"flex flex-col gap-3\"\n >\n {'groupLabel' in formattedGroup && formattedGroup.groupLabel && (\n <Text size=\"sm\" className=\"mt-2\" weight=\"semibold\">\n {formattedGroup.groupLabel}\n </Text>\n )}\n {formattedGroup.itens?.map((item: Item) =>\n renderCheckboxItem(item, categoryKey)\n )}\n </div>\n );\n\n // Helper component to render accordion trigger\n const renderAccordionTrigger = (\n category: CategoryConfig,\n isEnabled: boolean\n ) => {\n const allSelected = areAllFilteredItemsSelected(category.key);\n const someSelected = isMinimalOneCheckBoxIsSelected(category.key);\n\n return (\n <div className=\"flex items-center justify-between w-full p-2\">\n <div className=\"flex items-center gap-3\">\n <CheckBox\n checked={allSelected}\n disabled={!isEnabled}\n indeterminate={someSelected && !allSelected}\n onChange={() => toggleAllInCategory(category.key)}\n />\n <Text\n size=\"sm\"\n weight=\"medium\"\n className={cn('text-text-800', !isEnabled && 'opacity-40')}\n >\n {category.label}\n </Text>\n </div>\n {(openAccordion === category.key || isEnabled) && (\n <Badge variant=\"solid\" action=\"info\">\n {getBadgeText(category)}\n </Badge>\n )}\n </div>\n );\n };\n\n // Helper component to render compact single item view\n const renderCompactSingleItem = (category: CategoryConfig) => {\n const formattedItems = getFormattedItems(category.key);\n const allItems = formattedItems.flatMap((group) => group.itens || []);\n\n if (allItems.length !== 1) {\n return null;\n }\n\n const singleItem = allItems[0];\n\n return (\n <div\n key={category.key}\n className=\"flex items-center justify-between w-full px-3 py-2\"\n >\n <Text size=\"sm\" weight=\"bold\" className=\"text-text-800\">\n {category.label}\n </Text>\n <Text size=\"sm\" className=\"text-text-950\">\n {singleItem.name}\n </Text>\n </div>\n );\n };\n\n // Helper component to render category accordion\n const renderCategoryAccordion = (category: CategoryConfig) => {\n // Check if category is enabled based on dependencies\n const isEnabled = isCategoryEnabled(category);\n const hasOnlyOneItem = category.itens?.length === 1;\n\n if (hasOnlyOneItem && !compactSingleItem && !showSingleItem) {\n return null;\n }\n\n const formattedItems = getFormattedItems(category.key);\n const allItems = formattedItems.flatMap((group) => group.itens || []);\n const hasOnlyOneAvailableItem = allItems.length === 1;\n\n // If compactSingleItem is enabled and there's only one available item, render compact version\n if (compactSingleItem && hasOnlyOneAvailableItem && isEnabled) {\n return (\n <div key={category.key}>\n {renderCompactSingleItem(category)}\n {showDivider && <Divider />}\n </div>\n );\n }\n\n const hasNoItems = formattedItems.every(\n (group) => !group.itens || group.itens.length === 0\n );\n\n // Items visible to the user (dependency filter + active search)\n const displayGroups = getDisplayItems(category.key);\n\n const hasActiveSearch =\n !!category.searchable && !!searchQueries[category.key]?.trim();\n\n const hasNoSearchResults =\n hasActiveSearch &&\n !hasNoItems &&\n displayGroups.every((group) => !group.itens || group.itens.length === 0);\n\n return (\n <div key={category.key}>\n <CardAccordation\n value={category.key}\n disabled={!isEnabled}\n className={cn(\n 'bg-transparent border-0',\n openAccordion === category.key && 'bg-background-50 border-none'\n )}\n trigger={renderAccordionTrigger(category, isEnabled)}\n >\n <div className=\"flex flex-col gap-3 pt-2\">\n {category.searchable && !hasNoItems && (\n <Input\n type=\"text\"\n size=\"small\"\n aria-label=\"Buscar\"\n value={searchQueries[category.key] ?? ''}\n onChange={(e) =>\n handleSearchChange(category.key, e.target.value)\n }\n placeholder=\"Buscar\"\n iconLeft={<MagnifyingGlassIcon size={18} />}\n />\n )}\n {renderAccordionContent(\n category,\n isEnabled,\n hasNoItems,\n hasNoSearchResults,\n displayGroups\n )}\n </div>\n </CardAccordation>\n {openAccordion !== category.key && showDivider && <Divider />}\n </div>\n );\n };\n\n // Renders the item list (or an appropriate empty state) inside an accordion\n const renderAccordionContent = (\n category: CategoryConfig,\n isEnabled: boolean,\n hasNoItems: boolean,\n hasNoSearchResults: boolean,\n displayGroups: { groupLabel?: string; itens: Item[] }[]\n ) => {\n if (hasNoItems && isEnabled) {\n return (\n <div className=\"px-2 py-4\">\n <Text size=\"sm\" className=\"text-text-500 text-center\">\n Sem dados\n </Text>\n </div>\n );\n }\n\n if (hasNoSearchResults) {\n return (\n <div className=\"px-2 py-4\">\n <Text size=\"sm\" className=\"text-text-500 text-center\">\n Nenhum resultado encontrado\n </Text>\n </div>\n );\n }\n\n return displayGroups.map((formattedGroup, idx) =>\n renderFormattedGroup(formattedGroup, idx, category.key)\n );\n };\n\n // Auto-collapse accordion when category becomes disabled\n useEffect(() => {\n if (!openAccordion) return;\n\n const category = categories.find((c) => c.key === openAccordion);\n if (!category) return;\n\n // Check if the open category is now disabled\n const isEnabled = isCategoryEnabled(category);\n\n // If category is disabled, close it\n if (!isEnabled) {\n // Use setTimeout to ensure this runs after any other state updates\n setTimeout(() => {\n setOpenAccordion('');\n }, 0);\n }\n }, [categories, openAccordion]);\n\n return (\n <AccordionGroup\n type=\"single\"\n collapsible\n value={openAccordion}\n onValueChange={handleAccordionValueChange}\n >\n {categories.map(renderCategoryAccordion)}\n </AccordionGroup>\n );\n};\n","import {\n useState,\n useEffect,\n ReactNode,\n useMemo,\n useCallback,\n useSyncExternalStore,\n useRef,\n} from 'react';\nimport { Button, Modal, Stepper } from '../..';\nimport { CaretLeftIcon } from '@phosphor-icons/react/dist/csr/CaretLeft';\nimport { CaretRightIcon } from '@phosphor-icons/react/dist/csr/CaretRight';\nimport { PaperPlaneTiltIcon } from '@phosphor-icons/react/dist/csr/PaperPlaneTilt';\nimport { StepData } from '../Stepper/Stepper';\nimport { useAlertFormStore } from './useAlertForm';\nimport type { AlertsConfig, CategoryConfig } from '.';\nimport {\n MessageStep,\n RecipientsStep,\n DateStep,\n PreviewStep,\n} from './AlertSteps';\nimport {\n canFinish as canFinishValidation,\n isCurrentStepValid as isCurrentStepValidValidation,\n handleNext as handleNextValidation,\n} from './validation';\nimport { applyChainedAutoSelection } from '../shared/SendModalBase';\nimport { useDynamicStudentFetching } from '../../utils/useDynamicStudentFetching';\n\ninterface AlertsManagerProps {\n config: AlertsConfig;\n isOpen?: boolean;\n onClose?: () => void;\n /** URL da imagem após upload (prioritária - será exibida primeiro) */\n imageLink?: string | null;\n /** Imagem padrão a ser exibida quando não há imagem selecionada (deve ser URL string - o front deve converter File para URL se necessário) */\n defaultImage?: string | null;\n}\n\nconst StepWrapper = ({ children }: { children: ReactNode }) => (\n <div>{children}</div>\n);\n\nexport const AlertsManager = ({\n config,\n isOpen = false,\n onClose,\n imageLink,\n defaultImage,\n}: AlertsManagerProps) => {\n const [isModalOpen, setIsModalOpen] = useState(isOpen);\n const [currentStep, setCurrentStep] = useState(0);\n const [completedSteps, setCompletedSteps] = useState<number[]>([]);\n const [categories, setCategories] = useState(config.categories);\n\n // Subscribe to form store changes using useSyncExternalStore\n const formData = useSyncExternalStore(\n useAlertFormStore.subscribe,\n useAlertFormStore.getState\n );\n\n // Sincroniza com a prop isOpen\n useEffect(() => {\n setIsModalOpen(isOpen);\n }, [isOpen]);\n\n /**\n * Track if categories have been initialized for this modal session\n */\n const categoriesInitializedRef = useRef(false);\n\n const { labels, behavior, steps: customSteps } = config;\n\n /**\n * Handle categories change and fetch students dynamically if fetchStudentsByFilters is provided\n */\n const { handleCategoriesChange: baseHandleCategoriesChange } =\n useDynamicStudentFetching(setCategories, {\n fetchStudentsByFilters: behavior?.fetchStudentsByFilters,\n });\n\n const handleCategoriesChange = useCallback(\n async (updatedCategories: CategoryConfig[]) => {\n await baseHandleCategoriesChange(updatedCategories);\n },\n [baseHandleCategoriesChange]\n );\n\n /**\n * Initialize categories with auto-selection when modal opens\n */\n useEffect(() => {\n if (\n isModalOpen &&\n config.categories.length > 0 &&\n !categoriesInitializedRef.current\n ) {\n const autoSelectedCategories = applyChainedAutoSelection(\n config.categories\n );\n setCategories(autoSelectedCategories);\n\n // Trigger handleCategoriesChange to fetch students if needed\n // This is important when auto-selection happens (e.g., single school/series/class)\n // Note: previousSelectionsRef will be updated inside handleCategoriesChange\n void handleCategoriesChange(autoSelectedCategories);\n\n categoriesInitializedRef.current = true;\n }\n }, [\n isModalOpen,\n config.categories,\n applyChainedAutoSelection,\n handleCategoriesChange,\n ]);\n\n /**\n * Reset initialization flag when modal closes\n */\n useEffect(() => {\n if (!isModalOpen) {\n categoriesInitializedRef.current = false;\n }\n }, [isModalOpen]);\n\n // Steps padrão se não fornecidos\n const defaultSteps: StepData[] = [\n {\n id: '1',\n label: 'Mensagem',\n state: 'completed',\n },\n {\n id: '2',\n label: labels?.recipientsTitle || 'Destinatários',\n state: 'current',\n },\n {\n id: '3',\n label: labels?.dateLabel || 'Data de envio',\n state: 'pending',\n },\n {\n id: '4',\n label: labels?.previewTitle || 'Prévia',\n state: 'pending',\n },\n ];\n\n const steps = customSteps || defaultSteps;\n\n const handleCloseModal = () => {\n setIsModalOpen(false);\n onClose?.();\n };\n\n // Verifica se o step atual é válido\n const isCurrentStepValid = useCallback(() => {\n return isCurrentStepValidValidation(\n currentStep,\n formData,\n categories,\n customSteps\n );\n }, [currentStep, categories, customSteps, formData]);\n\n // Verifica se pode finalizar\n const canFinish = useCallback(() => {\n return canFinishValidation(formData, categories);\n }, [categories, formData]);\n\n const handleNext = useCallback(() => {\n const result = handleNextValidation({\n currentStep,\n steps,\n formData,\n categories,\n customSteps,\n completedSteps,\n setCompletedSteps,\n setCurrentStep,\n });\n\n if (!result.success && result.error) {\n alert(result.error);\n }\n }, [\n currentStep,\n steps,\n categories,\n customSteps,\n completedSteps,\n setCompletedSteps,\n setCurrentStep,\n formData,\n ]);\n\n const handlePrevious = useCallback(() => {\n if (currentStep > 0) {\n setCurrentStep(currentStep - 1);\n }\n }, [currentStep, setCurrentStep]);\n\n const handleFinish = async () => {\n if (!completedSteps.includes(currentStep)) {\n setCompletedSteps([...completedSteps, currentStep]);\n }\n\n const alertData = {\n title: formData.title,\n message: formData.message,\n image: formData.image,\n date: formData.date,\n time: formData.time,\n sendToday: formData.sendToday,\n sendCopyToEmail: formData.sendCopyToEmail,\n recipientCategories: Object.fromEntries(\n Object.entries(formData.recipientCategories).map(([key, c]) => [\n key,\n {\n selectedIds: c.selectedIds || [],\n allSelected: !!c.allSelected,\n },\n ])\n ),\n };\n\n try {\n if (behavior?.onSendAlert) {\n await behavior.onSendAlert(alertData);\n } else {\n console.log('Dados do formulário:', alertData);\n alert('Aviso enviado com sucesso!');\n }\n handleCloseModal();\n } catch (error) {\n console.error('Erro ao enviar aviso:', error);\n }\n };\n\n const dynamicSteps: StepData[] = (steps as StepData[]).map(\n (step, index: number) => {\n if (completedSteps.includes(index)) {\n return { ...step, state: 'completed' as const };\n }\n\n if (index === currentStep) {\n return { ...step, state: 'current' as const };\n }\n\n return { ...step, state: 'pending' as const };\n }\n );\n\n // Memoize step content to prevent re-renders and focus loss\n const currentStepContent = useMemo(() => {\n if (customSteps?.[currentStep]?.component) {\n const CustomComponent = customSteps[currentStep].component;\n return (\n <StepWrapper>\n <CustomComponent onNext={handleNext} onPrevious={handlePrevious} />\n </StepWrapper>\n );\n }\n\n switch (currentStep) {\n case 0:\n return (\n <StepWrapper>\n <MessageStep\n labels={labels}\n allowImageAttachment={behavior?.allowImageAttachment}\n />\n </StepWrapper>\n );\n case 1:\n return (\n <StepWrapper>\n <RecipientsStep\n categories={categories}\n labels={labels}\n onCategoriesChange={handleCategoriesChange}\n />\n </StepWrapper>\n );\n case 2:\n return (\n <StepWrapper>\n <DateStep\n labels={labels}\n allowScheduling={behavior?.allowScheduling}\n allowEmailCopy={behavior?.allowEmailCopy}\n />\n </StepWrapper>\n );\n case 3:\n return (\n <StepWrapper>\n <PreviewStep imageLink={imageLink} defaultImage={defaultImage} />\n </StepWrapper>\n );\n default:\n return null;\n }\n }, [\n currentStep,\n customSteps,\n categories,\n labels,\n behavior,\n handleNext,\n handlePrevious,\n imageLink,\n defaultImage,\n ]); // handleNext e handlePrevious agora estão nas dependências\n\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <Modal\n isOpen={isModalOpen}\n onClose={handleCloseModal}\n title={labels?.modalTitle || 'Enviar aviso'}\n size={'md'}\n contentClassName=\"p-0\"\n footer={\n <div className=\"flex gap-3 justify-end w-full\">\n <div className=\"flex gap-3\">\n <Button variant=\"link\" size=\"small\" onClick={handleCloseModal}>\n {labels?.cancelButton || 'Cancelar'}\n </Button>\n {!isFirstStep && (\n <Button\n variant=\"outline\"\n size=\"small\"\n iconLeft={<CaretLeftIcon />}\n onClick={handlePrevious}\n >\n {labels?.previousButton || 'Anterior'}\n </Button>\n )}\n {isLastStep ? (\n <Button\n variant=\"solid\"\n iconLeft={<PaperPlaneTiltIcon />}\n size=\"small\"\n onClick={handleFinish}\n disabled={!canFinish()}\n >\n {labels?.finishButton || 'Enviar Aviso'}\n </Button>\n ) : (\n <Button\n variant=\"solid\"\n iconRight={<CaretRightIcon />}\n size=\"small\"\n onClick={handleNext}\n disabled={!isCurrentStepValid()}\n >\n {labels?.nextButton || 'Próximo'}\n </Button>\n )}\n </div>\n </div>\n }\n >\n <div className=\"flex flex-col h-[calc(100vh-14rem)] max-h-[600px]\">\n {/* Stepper fixo no topo */}\n <div className=\"shrink-0 px-6 pt-4\">\n <Stepper\n steps={dynamicSteps}\n size=\"small\"\n showProgress\n responsive\n progressText={`Etapa ${currentStep + 1} de ${steps.length}`}\n />\n </div>\n\n {/* Área de conteúdo com scroll */}\n <div className=\"flex-1 overflow-y-auto px-6 pb-4\">\n {currentStepContent}\n </div>\n </div>\n </Modal>\n );\n};\n","import { useState } from 'react';\nimport { useShallow } from 'zustand/react/shallow';\nimport Input from '../../Input/Input';\nimport TextArea from '../../TextArea/TextArea';\nimport ImageUpload from '../../ImageUpload/ImageUpload';\nimport Text from '../../Text/Text';\nimport { useAlertFormStore } from '../useAlertForm';\nimport { LabelsConfig } from '../types';\n\ninterface MessageStepProps {\n labels?: LabelsConfig;\n allowImageAttachment?: boolean;\n}\n\nconst ACCEPTED_IMAGE_EXTENSIONS = '.jpg,.jpeg,.png,.gif,.webp,.svg';\nconst MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;\nconst MAX_IMAGE_SIZE_LABEL = '5MB';\n\nexport const MessageStep = ({\n labels,\n allowImageAttachment = true,\n}: MessageStepProps) => {\n const [imageError, setImageError] = useState<string | null>(null);\n const { title, message, image, setTitle, setMessage, setImage } =\n useAlertFormStore(\n useShallow((state) => ({\n title: state.title,\n message: state.message,\n image: state.image,\n setTitle: state.setTitle,\n setMessage: state.setMessage,\n setImage: state.setImage,\n }))\n );\n\n const handleFileSelect = (file: File) => {\n setImageError(null);\n setImage(file);\n };\n\n const handleRemoveFile = () => {\n setImageError(null);\n setImage(null);\n };\n\n const handleTypeError = () => {\n setImageError(\n 'Formato de imagem não suportado. Use jpg, jpeg, png, gif, webp ou svg.'\n );\n };\n\n const handleSizeError = () => {\n setImageError(\n `Imagem muito grande. O tamanho máximo permitido é ${MAX_IMAGE_SIZE_LABEL}.`\n );\n };\n\n const isImageFile = image instanceof File;\n return (\n <section className=\"flex flex-col gap-4\">\n <Input\n required\n label={labels?.titleLabel || 'Título'}\n placeholder={labels?.titlePlaceholder || 'Digite o título do aviso'}\n variant=\"rounded\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n\n <TextArea\n required\n label={labels?.messageLabel || 'Mensagem'}\n placeholder={labels?.messagePlaceholder || 'Digite a mensagem do aviso'}\n value={message}\n onChange={(e) => setMessage(e.target.value)}\n />\n\n {allowImageAttachment && (\n <div className=\"flex flex-col gap-2\">\n <ImageUpload\n selectedFile={isImageFile ? image : null}\n onFileSelect={handleFileSelect}\n onRemoveFile={handleRemoveFile}\n accept={ACCEPTED_IMAGE_EXTENSIONS}\n maxSize={MAX_IMAGE_SIZE_BYTES}\n onTypeError={handleTypeError}\n onSizeError={handleSizeError}\n />\n {imageError && (\n <Text size=\"sm\" className=\"text-error-600\" role=\"alert\">\n {imageError}\n </Text>\n )}\n </div>\n )}\n </section>\n );\n};\n","import { useEffect, useMemo } from 'react';\nimport { Text } from '../../..';\nimport { CheckboxGroup } from '../../CheckBoxGroup/CheckBoxGroup';\nimport { useAlertFormStore } from '../useAlertForm