From 6128f7d7dd89ccc387e66827fce5d43e3078208c Mon Sep 17 00:00:00 2001 From: Fernando Almeida Date: Sat, 4 Jul 2026 17:57:04 -0300 Subject: [PATCH 1/4] feat(etapa2-card-01): remove DashSimulado page, route and admin menu entry --- src/pages/dash/data.ts | 8 -- src/pages/dashSimulado/index.tsx | 171 ------------------------------- src/routes/PlatformRoutes.tsx | 12 --- src/routes/path.ts | 1 - 4 files changed, 192 deletions(-) delete mode 100644 src/pages/dashSimulado/index.tsx diff --git a/src/pages/dash/data.ts b/src/pages/dash/data.ts index db9389dd..bd57f137 100644 --- a/src/pages/dash/data.ts +++ b/src/pages/dash/data.ts @@ -14,7 +14,6 @@ import { DASH_PROVAS, DASH_QUESTION, DASH_ROLES, - DASH_SIMULADO, DASH_SUPPORT, DASH_PARTNER_SUPPORT, ESSAY_REVIEW_CURSINHO, @@ -197,13 +196,6 @@ export const adminMenuItems: DashCardMenu[] = [ link: `/dashboard/${ESSAY_REVIEW_LIST}`, permissions: [Roles.revisarTodasRedacoes], }, - { - icon: Matematica, - alt: "dash_simulado", - text: "Simulados", - link: `/dashboard/${DASH_SIMULADO}`, - permissions: [Roles.criarQuestao], - }, { icon: Historia, alt: "localiza cursinho", diff --git a/src/pages/dashSimulado/index.tsx b/src/pages/dashSimulado/index.tsx deleted file mode 100644 index 87f1d032..00000000 --- a/src/pages/dashSimulado/index.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { FilterProps } from "../../components/atoms/filter"; -import { SelectProps } from "../../components/atoms/select"; -import { OptionProps } from "../../components/atoms/selectOption"; -import { ButtonProps } from "../../components/molecules/button"; -import { CardDash } from "../../components/molecules/cardDash"; -import DashCardTemplate from "../../components/templates/dashCardTemplate"; -import { DashCardContext } from "../../context/dashCardContext"; -import { ISimuladoDTO } from "../../dtos/simulado/simuladoDto"; -import { StatusEnum } from "../../enums/generic/statusEnum"; -import { useToastAsync } from "../../hooks/useToastAsync"; -import { getSimulados } from "../../services/simulado/getSimulados"; -import { useAuthStore } from "../../store/auth"; -import { formatDate } from "../../utils/date"; -import { Paginate } from "../../utils/paginate"; - -const BLOQUEADO_ALL = "all"; -const BLOQUEADO_BLOCKED = "blocked"; -const BLOQUEADO_UNBLOCKED = "unblocked"; - -const bloqueadoOptions: OptionProps[] = [ - { id: BLOQUEADO_ALL, name: "Todos" }, - { id: BLOQUEADO_BLOCKED, name: "Bloqueados" }, - { id: BLOQUEADO_UNBLOCKED, name: "Liberados" }, -]; - -function DashSimulado() { - const [simulados, setSimulados] = useState([]); - const [nameFilter, setNameFilter] = useState(""); - const [categoriaFilter, setCategoriaFilter] = useState(""); - const [bloqueadoFilter, setBloqueadoFilter] = - useState(BLOQUEADO_ALL); - const [resetKey, setResetKey] = useState(0); - const { - data: { token }, - } = useAuthStore(); - const executeAsync = useToastAsync(); - const limitCards = 500; - - const cardTransformation = (simulado: ISimuladoDTO): CardDash => ({ - id: simulado._id, - title: simulado.nome, - status: simulado.bloqueado ? StatusEnum.Rejected : StatusEnum.Approved, - infos: [ - { field: "Categoria", value: simulado.categoria.nome }, - { - field: "Duracao", - value: simulado.categoria.duracao.toString() + " minutos", - }, - { - field: "Questoes", - value: `${simulado.questoes.length.toString()}/${(simulado.categoria.quantidadeTotalQuestao ?? 0).toString()}`, - }, - { - field: "Descrição", - value: - simulado.descricao && simulado.descricao.length > 20 - ? simulado.descricao.substring(0, 20) + "..." - : simulado.descricao, - }, - { - field: "Atualizado em ", - value: simulado.updatedAt - ? formatDate(simulado.updatedAt.toString()) - : "", - }, - ], - }); - - useEffect(() => { - executeAsync({ - action: () => getSimulados(token, 1, limitCards), - loadingMessage: "Buscando Simulados...", - successMessage: "Simulados carregados com sucesso!", - errorMessage: (error) => error.message, - onSuccess: (res) => setSimulados(res.data), - onError: () => setSimulados([]), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [token]); - - const getMoreCards = async ( - page: number - ): Promise> => { - return await getSimulados(token, page, limitCards); - }; - - const categoriaOptions: OptionProps[] = useMemo(() => { - const names = new Set(); - simulados.forEach((s) => { - if (s.categoria?.nome) names.add(s.categoria.nome); - }); - return [ - { id: "", name: "Todos os tipos" }, - ...Array.from(names) - .sort((a, b) => a.localeCompare(b)) - .map((n) => ({ id: n, name: n })), - ]; - }, [simulados]); - - const filteredSimulados = useMemo(() => { - const term = nameFilter.trim().toLowerCase(); - return simulados.filter((s) => { - if (term && !s.nome.toLowerCase().includes(term)) return false; - if (categoriaFilter && s.categoria?.nome !== categoriaFilter) return false; - if (bloqueadoFilter === BLOQUEADO_BLOCKED && !s.bloqueado) return false; - if (bloqueadoFilter === BLOQUEADO_UNBLOCKED && s.bloqueado) return false; - return true; - }); - }, [simulados, nameFilter, categoriaFilter, bloqueadoFilter]); - - const filterProps: FilterProps = { - placeholder: "Buscar por nome", - filtrar: (e: React.ChangeEvent) => - setNameFilter(e.target.value), - defaultValue: nameFilter, - }; - - const selectFiltes: SelectProps[] = [ - { - options: categoriaOptions, - defaultValue: categoriaFilter, - setState: (value: string) => setCategoriaFilter(value), - }, - { - options: bloqueadoOptions, - defaultValue: bloqueadoFilter, - setState: (value: string) => setBloqueadoFilter(value), - }, - ]; - - const hasActiveFilters = - nameFilter !== "" || categoriaFilter !== "" || bloqueadoFilter !== BLOQUEADO_ALL; - - const buttons: ButtonProps[] = [ - { - onClick: () => { - setNameFilter(""); - setCategoriaFilter(""); - setBloqueadoFilter(BLOQUEADO_ALL); - setResetKey((k) => k + 1); - }, - typeStyle: "quaternary", - size: "small", - disabled: !hasActiveFilters, - children: "Limpar filtros", - }, - ]; - - return ( - {}, - getMoreCards, - cardTransformation, - limitCards, - filterProps, - selectFiltes, - buttons, - totalItems: filteredSimulados.length, - }} - > - - - ); -} - -export default DashSimulado; diff --git a/src/routes/PlatformRoutes.tsx b/src/routes/PlatformRoutes.tsx index 5579dd09..b9f9a4a2 100644 --- a/src/routes/PlatformRoutes.tsx +++ b/src/routes/PlatformRoutes.tsx @@ -39,7 +39,6 @@ import DashNews from "../pages/dashNews"; import DashProva from "../pages/dashProvas"; import DashQuestionNew from "../pages/dashQuestionNew"; import DashRoles from "../pages/dashRoles"; -import DashSimulado from "../pages/dashSimulado"; import Forgot from "../pages/forgot"; import Login from "../pages/login"; import Logout from "../pages/logout"; @@ -72,7 +71,6 @@ import { DASH_PROVAS, DASH_QUESTION, DASH_ROLES, - DASH_SIMULADO, DASH_SUPPORT, DASH_PARTNER_SUPPORT, DECLARED_INTEREST, @@ -343,16 +341,6 @@ export function PlatformRoutes() { } /> - - - - } - /> Date: Sat, 4 Jul 2026 21:03:31 -0300 Subject: [PATCH 2/4] feat(etapa2-card-02): add getProvaById service and SimuladoResumo DTO --- src/dtos/prova/prova.ts | 12 ++++++++++++ src/services/prova/getProvaById.ts | 15 +++++++++++++++ src/services/urls.ts | 1 + 3 files changed, 28 insertions(+) create mode 100644 src/services/prova/getProvaById.ts diff --git a/src/dtos/prova/prova.ts b/src/dtos/prova/prova.ts index f53ece05..b7b51df0 100644 --- a/src/dtos/prova/prova.ts +++ b/src/dtos/prova/prova.ts @@ -1,6 +1,17 @@ import { DateTime } from "luxon"; +import { ICategoria } from "../categoria/categoria"; import { Edicao } from "../../enums/prova/edicao"; +export interface SimuladoResumo { + _id: string; + nome: string; + categoria: ICategoria; + questoes: { _id: string }[]; + bloqueado: boolean; + aproveitamento?: number; + vezesRespondido?: number; +} + export interface Prova { _id: string; edicao: Edicao; @@ -16,6 +27,7 @@ export interface Prova { filename: string; gabarito: string; enemAreas: string[]; + simulados?: SimuladoResumo[]; } export interface CreateProva { diff --git a/src/services/prova/getProvaById.ts b/src/services/prova/getProvaById.ts new file mode 100644 index 00000000..e5ddaf85 --- /dev/null +++ b/src/services/prova/getProvaById.ts @@ -0,0 +1,15 @@ +import { Prova } from "../../dtos/prova/prova"; +import fetchWrapper from "../../utils/fetchWrapper"; +import { provaById } from "../urls"; + +export async function getProvaById(id: string, token: string): Promise { + const response = await fetchWrapper(provaById(id), { + method: "GET", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + }); + const res = await response.json(); + if (response.status !== 200) { + throw new Error(`Erro ao buscar Prova ${res.message}`); + } + return res; +} diff --git a/src/services/urls.ts b/src/services/urls.ts index 789deedd..646e6be8 100644 --- a/src/services/urls.ts +++ b/src/services/urls.ts @@ -54,6 +54,7 @@ export const answer = `${simulado}/answer`; export const report = `${simulado}/report`; export const prova = `${mssimulado}/prova`; +export const provaById = (id: string) => `${prova}/${id}`; export const missing = `${prova}/missing`; export const questoes = `${mssimulado}/questoes`; export const historico = `${mssimulado}/historico`; From 5d3abf106bb21f85ad2b282c5a2c49a52cd9620f Mon Sep 17 00:00:00 2001 From: Fernando Almeida Date: Sun, 5 Jul 2026 00:29:33 -0300 Subject: [PATCH 3/4] feat(etapa2-card-03): add ShowProva dual-view (details/simulados) and fix Prova DTO categoria type --- src/dtos/prova/prova.ts | 3 +- src/pages/dashProvas/modals/showProva.tsx | 392 ++++++++++-------- src/pages/dashProvas/modals/simuladosView.tsx | 91 ++++ 3 files changed, 312 insertions(+), 174 deletions(-) create mode 100644 src/pages/dashProvas/modals/simuladosView.tsx diff --git a/src/dtos/prova/prova.ts b/src/dtos/prova/prova.ts index b7b51df0..a97bb23a 100644 --- a/src/dtos/prova/prova.ts +++ b/src/dtos/prova/prova.ts @@ -17,8 +17,7 @@ export interface Prova { edicao: Edicao; aplicacao: number; ano: number; - categoria: string; - exame: string; + categoria: ICategoria; nome: string; totalQuestao: number; totalQuestaoCadastradas: number; diff --git a/src/pages/dashProvas/modals/showProva.tsx b/src/pages/dashProvas/modals/showProva.tsx index 7127a356..d4bc29d1 100644 --- a/src/pages/dashProvas/modals/showProva.tsx +++ b/src/pages/dashProvas/modals/showProva.tsx @@ -6,15 +6,18 @@ import { ArrowDownTrayIcon, ChartBarIcon, DocumentTextIcon, - PencilSquareIcon + PencilSquareIcon, + TableCellsIcon, } from "@heroicons/react/24/outline"; import { Button } from "@mui/material"; import { toast } from "react-toastify"; import { Prova } from "../../../dtos/prova/prova"; import { getProvaFile } from "../../../services/prova/getFile"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useToastAsync } from "@/hooks/useToastAsync"; import { updateProvaFiles } from "@/services/prova/updateProvaFiles"; +import { getProvaById } from "../../../services/prova/getProvaById"; +import SimuladosView from "./simuladosView"; import UploadButton from "../../../components/molecules/uploadButton"; interface ShowProvaProps { @@ -30,6 +33,13 @@ function ShowProva({ prova, isOpen, handleClose, onUpdated }: ShowProvaProps) { const [newFile, setNewFile] = useState(null); const [newGabarito, setNewGabarito] = useState(null); + type ShowProvaView = 'details' | 'simulados'; + const [view, setView] = useState('details'); + const [fullProva, setFullProva] = useState(null); + const [loadingSimulados, setLoadingSimulados] = useState(false); + const [errorSimulados, setErrorSimulados] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const percentCadastradas = (prova.totalQuestaoCadastradas / prova.totalQuestao) * 100; const percentValidadas = @@ -45,6 +55,22 @@ function ShowProva({ prova, isOpen, handleClose, onUpdated }: ShowProvaProps) { return "bg-red-500"; }; + useEffect(() => { + if (!isOpen) return; + let cancelled = false; + setLoadingSimulados(true); + setErrorSimulados(null); + getProvaById(prova._id, token) + .then(data => { if (!cancelled) setFullProva(data); }) + .catch(err => { if (!cancelled) setErrorSimulados(err.message); }) + .finally(() => { if (!cancelled) setLoadingSimulados(false); }); + return () => { cancelled = true; }; + }, [isOpen, prova._id, token, retryCount]); + + useEffect(() => { + if (!isOpen) setView('details'); + }, [isOpen]); + const downloadFile = async (filename: string, fileType: string) => { const id = toast.loading(`Baixando ${fileType}...`); @@ -157,198 +183,220 @@ const downloadFile = async (filename: string, fileType: string) => { - {/* Informações Básicas */} -
-

- - Informações Gerais -

-
-
-

Edição

-

{prova.edicao}

-
-
-

Ano

-

{prova.ano}

-
-
-

Aplicação

-

{prova.aplicacao}

-
-
-

Categoria

-

{prova.categoria}

- {prova.exame && ( -

{prova.exame}

- )} -
-
-
- - {/* Métricas de Progresso */} -
-

- - Progresso das Questões -

- -
- {/* Questões Esperadas */} -
-
- - Questões Esperadas - - - {prova.totalQuestao} - -
-
-
+ {view === 'details' ? ( + <> + {/* Informações Básicas */} +
+

+ + Informações Gerais +

+
+
+

Edição

+

{prova.edicao}

+
+
+

Ano

+

{prova.ano}

+
+
+

Aplicação

+

{prova.aplicacao}

+
+
+

Categoria

+

{prova.categoria?.nome ?? '—'}

+ {prova.categoria?.exame?.nome && ( +

{prova.categoria.exame.nome}

+ )} +
- {/* Questões Cadastradas */} -
-
- - Questões Cadastradas - - - {prova.totalQuestaoCadastradas} ( - {percentCadastradas.toFixed(1)}%) - -
-
-
-
-
+ {/* Métricas de Progresso */} +
+

+ + Progresso das Questões +

+ +
+ {/* Questões Esperadas */} +
+
+ + Questões Esperadas + + + {prova.totalQuestao} + +
+
+
+
+
- {/* Questões Aprovadas */} -
-
- - Questões Aprovadas - - - {prova.totalQuestaoValidadas} ( - {isNaN(percentValidadas) ? "0" : percentValidadas.toFixed(1)} - %) - -
-
-
+ {/* Questões Cadastradas */} +
+
+ + Questões Cadastradas + + + {prova.totalQuestaoCadastradas} ( + {percentCadastradas.toFixed(1)}%) + +
+
+
+
+
+ + {/* Questões Aprovadas */} +
+
+ + Questões Aprovadas + + + {prova.totalQuestaoValidadas} ( + {isNaN(percentValidadas) ? "0" : percentValidadas.toFixed(1)} + %) + +
+
+
+
+
-
-
- {/* Botões de Download */} -
- {!isEditingFiles ? ( - <> - {prova.gabarito && ( + {/* Botões de Download */} +
+ {!isEditingFiles ? ( + <> + + {prova.gabarito && ( + + )} + + - )} - - - - - - ) : ( -
-
- - - -
- -
- + + ) : ( +
+
+ + + +
+ +
+ + + +
-
- )} -
+ )} +
+ + ) : ( + setView('details')} + onRetry={() => setRetryCount(c => c + 1)} + /> + )}
); diff --git a/src/pages/dashProvas/modals/simuladosView.tsx b/src/pages/dashProvas/modals/simuladosView.tsx new file mode 100644 index 00000000..2366109e --- /dev/null +++ b/src/pages/dashProvas/modals/simuladosView.tsx @@ -0,0 +1,91 @@ +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import { SimuladoResumo } from "../../../dtos/prova/prova"; + +interface SimuladosViewProps { + simulados: SimuladoResumo[] | undefined; + loading: boolean; + error: string | null; + onVoltar: () => void; + onRetry: () => void; +} + +function SimuladosView({ simulados, loading, error, onVoltar, onRetry }: SimuladosViewProps) { + return ( +
+ + +

Simulados

+ + {loading && ( +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+ )} + + {!loading && error && ( +
+

Não foi possível carregar os simulados.

+ +
+ )} + + {!loading && !error && simulados?.length === 0 && ( +

+ Esta prova ainda não tem simulados cadastrados. +

+ )} + + {!loading && !error && simulados && simulados.length > 0 && ( +
+ + + + + + + + + + + + + {simulados.map((simulado) => ( + + + + + + + + + ))} + +
NomeCategoriaQuestõesAproveitamentoRespondidoStatus
{simulado.nome} + {simulado.categoria?.nome ?? '—'} + + {simulado.questoes.length}/{simulado.categoria?.quantidadeTotalQuestao ?? '-'} + + {simulado.aproveitamento ?? 0} + + {simulado.vezesRespondido ?? 0}x + + {simulado.bloqueado ? '🔒 Bloqueado' : '✅ Liberado'} +
+
+ )} +
+ ); +} + +export default SimuladosView; From 3969c2cae67164d0739d3acf1a590a8c1435a3ed Mon Sep 17 00:00:00 2001 From: Fernando Almeida Date: Sun, 5 Jul 2026 01:52:26 -0300 Subject: [PATCH 4/4] feat(etapa2-card-06): remove criarSimulado from frontend and delete orphan getSimulados service - Remove criarSimulado from Roles enum and RolesLabel array - Remove criarSimulado field from CreateRoleDto interface - Remove criarSimulado: false from EMPTY_ROLE in both modal components - Delete orphaned src/services/simulado/getSimulados.ts (zero callers) Note: getSimuladosDefaults.ts is preserved as it has callers Co-Authored-By: Claude Sonnet 4.6 --- src/dtos/roles/createRole.ts | 1 - src/enums/roles/roles.ts | 6 ------ src/pages/dashRoles/modals/ModalNewRole.tsx | 1 - .../managerCollaborator/modals/ModalNewRole.tsx | 1 - src/services/simulado/getSimulados.ts | 15 --------------- 5 files changed, 24 deletions(-) delete mode 100644 src/services/simulado/getSimulados.ts diff --git a/src/dtos/roles/createRole.ts b/src/dtos/roles/createRole.ts index 5e5f9a2b..d6c49c99 100644 --- a/src/dtos/roles/createRole.ts +++ b/src/dtos/roles/createRole.ts @@ -4,7 +4,6 @@ export interface CreateRoleDto { roleBase?: string; validarCursinho: boolean; alterarPermissao: boolean; - criarSimulado: boolean; visualizarQuestao: boolean; criarQuestao: boolean; validarQuestao: boolean; diff --git a/src/enums/roles/roles.ts b/src/enums/roles/roles.ts index 7af51ad4..306f98d9 100644 --- a/src/enums/roles/roles.ts +++ b/src/enums/roles/roles.ts @@ -1,7 +1,6 @@ export enum Roles { validarCursinho = "validarCursinho", alterarPermissao = "alterarPermissao", - criarSimulado = "criarSimulado", visualizarQuestao = "visualizarQuestao", criarQuestao = "criarQuestao", validarQuestao = "validarQuestao", @@ -41,11 +40,6 @@ export const RolesLabel = [ label: "Alterar Permissões", isProjectPermission: true, }, - { - value: Roles.criarSimulado, - label: "Visualizar Simulados", - isProjectPermission: true, - }, { value: Roles.visualizarQuestao, label: "Visualizar Questões", diff --git a/src/pages/dashRoles/modals/ModalNewRole.tsx b/src/pages/dashRoles/modals/ModalNewRole.tsx index 366dc87a..3f402cec 100644 --- a/src/pages/dashRoles/modals/ModalNewRole.tsx +++ b/src/pages/dashRoles/modals/ModalNewRole.tsx @@ -28,7 +28,6 @@ const EMPTY_ROLE: CreateRoleDto = { base: false, validarCursinho: false, alterarPermissao: false, - criarSimulado: false, visualizarQuestao: false, criarQuestao: false, validarQuestao: false, diff --git a/src/pages/managerCollaborator/modals/ModalNewRole.tsx b/src/pages/managerCollaborator/modals/ModalNewRole.tsx index ab75fd87..66ae848f 100644 --- a/src/pages/managerCollaborator/modals/ModalNewRole.tsx +++ b/src/pages/managerCollaborator/modals/ModalNewRole.tsx @@ -31,7 +31,6 @@ const EMPTY_ROLE: CreateRoleDto = { roleBase: "", validarCursinho: false, alterarPermissao: false, - criarSimulado: false, visualizarQuestao: false, criarQuestao: false, validarQuestao: false, diff --git a/src/services/simulado/getSimulados.ts b/src/services/simulado/getSimulados.ts deleted file mode 100644 index 88af20ab..00000000 --- a/src/services/simulado/getSimulados.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ISimuladoDTO } from "../../dtos/simulado/simuladoDto"; -import fetchWrapper from "../../utils/fetchWrapper"; -import { Paginate } from "../../utils/paginate"; -import { simulado } from "../urls"; - -export async function getSimulados(token: string, page: number = 1, limit: number = 40) : Promise> { - const response = await fetchWrapper(`${simulado}?page=${page}&limit=${limit}`, { - method: "GET", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, - }) - if(response.status !== 200){ - throw new Error('Erro ao buscar simulado') - } - return await response.json() -} \ No newline at end of file