diff --git a/GEMINI.md b/GEMINI.md
index 1e78788..97b9611 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -79,5 +79,9 @@
46. **Geração de Contratos Automáticos (Fase SQL-First)**: A geração de contratos no cadastro de alunos deve sincronizar diretamente no banco de dados com `POST /api/contratos` e manter fallbacks no `updateData`. Além disso, para evitar contratos duplicados acidentais, o checkbox `generateContract` deve ser desmarcado por padrão ao editar um aluno, mas marcado por padrão ao cadastrar um novo aluno. O checkbox de "Gerar Taxa" foi removido.
47. **Ficha de Frequência Geral (Roster de Turma) em PDF**: O botão de exportação de PDF na lista de alunos da turma no módulo Registro de Frequência do Manager deve exportar um relatório de frequência geral de todos os alunos ativos da turma, com a contagem total acumulada de presenças (P), faltas (F), justificadas (J) e porcentagem de frequência (%) de forma a bater perfeitamente com os dados exibidos na tela, evitando PDFs vazios ou em branco para datas sem aulas.
-48. **Pré-Matrícula - Folder Promocional, Rascunho e Duplicatas**: O painel de administração permite que o administrador faça o upload de um folder/banner de promoção (flyer) que é salvo diretamente no bucket `prematriculas` do MinIO. Na página pública de pré-matrícula, se configurado, esse banner é renderizado de forma responsiva acima do formulário. Se o formulário estiver em modo rascunho (`draft`), a página pública oculta todas as turmas e exibe uma mensagem polida informando que todas as vagas estão lotadas e novas vagas abrirão em breve. Inscrições duplicadas são bloqueadas via validação por e-mail e telefone higienizado no backend.
+48. **Pré-Matrícula - Folder Promocional Dinâmico (Banner), Rascunho e Duplicatas**: O banner de promoção (flyer) deve ser gerenciado de forma 100% dinâmica como um campo personalizado na lista de campos do formulário (tipo `banner`). Isso permite que o administrador altere a ordem de exibição do flyer em relação aos demais campos e o exclua/adicione livremente. Se o formulário estiver em modo rascunho (`draft`), a página pública oculta as turmas e exibe uma mensagem elegante de vagas esgotadas. Duplicatas são bloqueadas no backend.
+49. **Pré-Matrícula - Soft Delete de Campos e Preservação de Histórico**: É estritamente proibido realizar exclusões permanentes (hard-delete) de campos personalizados do formulário de pré-matrícula para evitar a exibição de códigos UUID orfãos na lista de inscrições e na exportação CSV de matrículas antigas. A exclusão de campos deve utilizar Soft Delete (`ativo = false`), e o frontend deve utilizar um mapeador inteligente de fallbacks para recuperar rótulos legados e campos inativos.
+50. **Pré-Matrícula - Máscaras de Entrada e Single Event Binding**: Para evitar comportamentos instáveis em teclados móveis (como Gboard no Android ou Autoresumos no iOS), as máscaras de campos estruturados (telefone, CPF, CEP) devem ser gerenciadas por um único escopo de eventos global (`document.addEventListener`). É estritamente proibido realizar vinculações duplas (Double Binding) com ouvintes locais diretos concorrentes nos elementos dinâmicos, para evitar a perda de foco, cursor saltando, ou substituição indesejada de caracteres digitados. A máscara de telefone deve diferenciar e suportar dinamicamente tanto telefones fixos (10 dígitos) quanto celulares (11 dígitos).
+
+
diff --git a/MEMORY.md b/MEMORY.md
index 6a7c9da..2af0719 100644
--- a/MEMORY.md
+++ b/MEMORY.md
@@ -70,9 +70,16 @@ Nesta sessão de trabalho, realizamos o "core" da transição do sistema EduMana
- **Relatório de Frequência Geral em PDF (Manager)**: Refatoração do exportador de PDF no módulo Registro de Frequência para gerar uma Ficha de Frequência Geral (Roster de todos os alunos ativos da turma, com contagem acumulada de presenças, faltas, justificativas e porcentagem de frequência de cada um) de modo a corresponder perfeitamente ao que o administrador visualiza na modal, eliminando downloads vazios.
### Fase 9: Endurecimento da Pré-Matrícula, Upload de Panfleto Promocional e Proteção contra Duplicatas
-- **Upload de Folder Promocional (MinIO)**: Adição da coluna `banner_url` na tabela SQL `prematricula_config`. Criada a área de upload no painel do administrador para subir folders promocionais diretamente para a pasta `prematriculas` no MinIO Storage local, com pré-visualização em tempo real e remoção inteligente.
-- **Renderização Pública Premium**: O folder promocional carregado pelo administrador é renderizado em destaque no topo do formulário público com cantos arredondados, limite de tamanho responsivo e sombras sutis.
+- **Folder Promocional Dinâmico e Reordenável (Banner)**: Refatorado o folder promocional (flyer) para ser tratado de forma 100% dinâmica como um campo personalizado especial (`tipo: 'banner'`) dentro do construtor de formulários. O administrador pode fazer o upload da imagem promocional diretamente no construtor (no modal de novo campo ou de edição), excluir e reordenar sua posição de exibição livremente em relação aos outros campos.
+- **Renderização Pública Inline Premium**: A página pública de pré-matrícula renderiza a imagem do banner inline de forma premium, responsiva e exatamente na posição sequencial configurada na listagem de campos do banco.
- **Prevenção de Duplicatas e Spam**: Bloqueio de envios duplicados no backend comparando e-mail e telefone limpos de caracteres não-numéricos (para total equivalência).
- **Tratamento de Estado Rascunho (`draft`)**: Se a configuração estiver em rascunho, exibe automaticamente uma página informativa de "Vagas Lotadas - Novas vagas em breve" para proteger a integridade operacional antes do lançamento oficial.
- **Gestão de Campos Personalizados com Arquivos**: Implementação de tipo `file` para campos customizados no formulário do aluno, com upload assíncrono transparente via `FormData` e exibição em formato de hiperlink nos detalhes da inscrição para o administrador.
+- **Preservação de Histórico de Campos Personalizados (Soft Delete)**: Correção de bug onde a exclusão de um campo (ex: Sexo) fazia com que o rótulo do campo sumisse e exibisse o código UUID bruto na listagem de inscritos e na exportação de CSV. Implementado Soft Delete (`ativo = false`) na exclusão e criado um mapeador inteligente com dicionário de fallbacks (`getFriendlyLabel`) que recupera os rótulos de campos excluídos ou legados em tempo real.
+- **Correção de Máscaras de Entrada (Telefone/CPF/CEP)**: Correção de bug em teclados móveis (Android Gboard/iOS Safari) onde caracteres digitados desapareciam ou geravam loops repetitivos (ex: `(( () ()- 0-`). O problema era causado por double event binding (dois ouvintes `input` concorrentes ativos no mesmo elemento). Centralizada a máscara no ouvinte global de formulário e aprimorada a máscara de telefone para suportar nativamente tanto números fixos (10 dígitos: `(XX) XXXX-XXXX`) quanto móveis (11 dígitos: `(XX) XXXXX-XXXX`).
+- **Exclusão Automática Pós-Conversão**: Implementada a exclusão automática da inscrição de pré-matrícula quando a mesma é concluída com sucesso e convertida em matrícula ativa de aluno no sistema. Captura o ID da pré-matrícula original de forma resiliente e dispara uma requisição `DELETE` contra `/api/prematricula/inscricoes/:id`, limpando instantaneamente a lista de pendências da aba de Pré-Matrícula sem requerer intervenção manual.
+- **Favicon Dinâmico e Logos Arredondados**: Implementação de crop dinâmico em tempo de execução usando canvas HTML5 para gerar favicons perfeitamente circulares (`rounded-full`) a partir do logotipo cadastrado nas configurações da escola. O favicon circular dinâmico é atualizado automaticamente na aba do navegador tanto no painel administrativo do gestor quanto na página pública de pré-matrícula. Adicionalmente, todos os logotipos exibidos no painel do administrador (Sidebar) e na página pública foram estilizados com bordas elegantes, sombras sutis e arredondamento perfeito (`rounded-full w-8 h-8 object-cover`), elevando o alinhamento visual e a identidade estética premium da plataforma.
+- **Favicon Dinâmico e Suporte PWA no Portal do Aluno**: Estendi a geração de favicons perfeitamente circulares (`rounded-full`) via canvas HTML5 para o Portal do Aluno, assegurando correspondência e marca unificada em toda a experiência do estudante. O logotipo da escola também foi estilizado como um círculo perfeito (`rounded-full object-cover`) na barra lateral (`Sidebar.tsx`) e na tela de Login do Portal (`Login.tsx`). Além disso, implementei suporte nativo e dinâmico a PWA (Progressive Web App) no Portal do Aluno: a API agora gera dinamicamente o arquivo `manifest.json` com o nome personalizado e logotipo da escola, registra o Service Worker (`sw.js`) em segundo plano para atender aos critérios de instalação de navegadores e renderiza um botão premium de glassmorphism verde pulsante e micro-animado ("Instalar App") na Sidebar no momento em que o aplicativo se torna instalável, permitindo aos alunos instalar o EduManager em seus celulares/computadores como um aplicativo nativo.
+- **Correção da Persistência do Valor (Pontuação Máxima) das Avaliações**: Corrigido bug crítico no módulo de avaliações (Provas/Atividades) onde alterações na pontuação máxima ("Valor" ou `maxScore`) não eram salvas no banco de dados e resetavam de volta para 10.0 ao recarregar a página. A falha ocorria devido à ausência da coluna `max_score` nas operações de INSERT e UPDATE da camada de serviço (`database.js`), bem como a falta de mapeamento do campo no mapeador de retorno (`getProvas`) e no estado do React (`Exams.tsx` e `ReportCard.tsx`). Mapeei dinamicamente a propriedade `maxScore` em todos os fluxos da aplicação (Manager e Portal do Aluno), encapsulada pela conversão estrita `Number(p.max_score)` em conformidade com a Regra 19 de integridade numérica, garantindo que as pontuações e notas correspondentes no Boletim Escolar e Portal do Aluno reflitam precisamente o valor personalizado no banco de dados.
+- **Nova Regra de Cálculo de Notas (Soma Capped 10 & Média por Período)**: Refatorei por completo a lógica de cálculo de notas do boletim escolar no Manager (`ReportCard.tsx`) e no Portal do Aluno (`Notas.tsx`). Agora, a nota de um aluno em um período para uma determinada disciplina é calculada pela **SOMA** das notas obtidas em todas as avaliações (provas e atividades) vinculadas àquele período, **limitada ao valor máximo de 10.0** (cap a 10). A Média Geral do aluno passou a ser calculada a nível de período: primeiro é calculada a média aritmética de todas as disciplinas daquele período específico e, caso existam múltiplos períodos com notas, a Média Geral do aluno é calculada pela média aritmética dos períodos lançados; caso exista apenas um único período ativo, a média geral é a média aritmética das disciplinas daquele único período.
diff --git a/manager/components/Exams.tsx b/manager/components/Exams.tsx
index d168e57..0602e55 100644
--- a/manager/components/Exams.tsx
+++ b/manager/components/Exams.tsx
@@ -41,6 +41,7 @@ const Exams: React.FC = ({ data, updateData }) => {
allowRetake: p.allowRetake ?? p.permitir_refacao ?? false,
isDeleted: p.isDeleted ?? p.is_deleted ?? false,
evaluationType: p.evaluationType || p.evaluation_type || 'exam',
+ maxScore: p.maxScore !== undefined && p.maxScore !== null ? Number(p.maxScore) : (p.max_score !== undefined && p.max_score !== null ? Number(p.max_score) : 10),
questions: p.questions || []
})));
}
diff --git a/manager/components/PreMatricula.tsx b/manager/components/PreMatricula.tsx
index 85f40b5..39f002d 100644
--- a/manager/components/PreMatricula.tsx
+++ b/manager/components/PreMatricula.tsx
@@ -15,7 +15,22 @@ interface Props {
const FIELD_TYPES: Record = {
text: 'Texto', email: 'Email', phone: 'Telefone', cpf: 'CPF',
date: 'Data', select: 'Seleção', textarea: 'Texto Longo', number: 'Número',
- file: 'Imagem/Documento'
+ banner: 'Folder / Panfleto de Promoção (Banner)'
+};
+
+const getFriendlyLabel = (key: string, value: any): string => {
+ if (key === 'da80bac4-4018-4ee9-a9c4-a25551411269' || key === 'da80bac4-4018-4ee9-a9c4-a25551411269'.replace(/-/g, '')) return 'Sexo';
+ if (key === '09e08119-02e1-49cf-8a48-eb7b51a35758' || key === '09e08119-02e1-49cf-8a48-eb7b51a35758'.replace(/-/g, '')) return 'Sexo';
+ if (key === '82bcc4ce-8ece-4a1b-8131-e64585119cf4' || key === '82bcc4ce-8ece-4a1b-8131-e64585119cf4'.replace(/-/g, '')) return 'CPF';
+
+ const valStr = String(value || '');
+ if (valStr === 'Feminino' || valStr === 'Masculino' || valStr === 'Outro') {
+ return 'Sexo';
+ }
+ if (/^\d{11}$/.test(valStr) || /^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(valStr)) {
+ return 'CPF';
+ }
+ return 'Campo Excluído';
};
const DAY_NAMES = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado'];
@@ -23,6 +38,7 @@ const DAY_NAMES = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', '
const PreMatricula: React.FC = ({ data, onConvert }) => {
const [config, setConfig] = useState(null);
const [campos, setCampos] = useState([]);
+ const [todosCampos, setTodosCampos] = useState([]);
const [inscricoes, setInscricoes] = useState([]);
const [turmas, setTurmas] = useState([]);
const [activeTab, setActiveTab] = useState<'editor' | 'inscricoes'>('editor');
@@ -44,7 +60,7 @@ const PreMatricula: React.FC = ({ data, onConvert }) => {
fetch('/api/turmas')
]);
if (cfgRes.ok) { const j = await cfgRes.json(); setConfig(j.config); }
- if (camposRes.ok) { const j = await camposRes.json(); setCampos(j.campos || []); }
+ if (camposRes.ok) { const j = await camposRes.json(); setCampos(j.campos || []); setTodosCampos(j.todosCampos || j.campos || []); }
if (inscRes.ok) { const j = await inscRes.json(); setInscricoes(j.inscricoes || []); }
if (turmasRes.ok) { const j = await turmasRes.json(); setTurmas(j.turmas || []); }
} catch (e) { console.error(e); }
@@ -156,7 +172,10 @@ const PreMatricula: React.FC = ({ data, onConvert }) => {
if (filtered.length === 0) return;
const allKeys = new Set();
filtered.forEach(i => Object.keys(i.respostas || {}).forEach(k => allKeys.add(k)));
- const headers = ['Nome', 'Email', 'Telefone', 'Turma', 'Status', 'Data', ...Array.from(allKeys)];
+ const headers = ['Nome', 'Email', 'Telefone', 'Turma', 'Status', 'Data', ...Array.from(allKeys).map(k => {
+ const field = todosCampos.find(c => c.id === k);
+ return field ? field.label : getFriendlyLabel(k, null);
+ })];
const rows = filtered.map(i => [
i.nome, i.email, i.telefone, i.turmaNome, i.status,
new Date(i.createdAt).toLocaleDateString('pt-BR'),
@@ -388,61 +407,6 @@ const PreMatricula: React.FC = ({ data, onConvert }) => {
Se nenhuma turma for adicionada, todas as turmas ativas aparecerão para seleção do aluno. Defina o limite de vagas (deixe em branco para ilimitado).