feat: add dynamic field toggles, phone/cpf/cep input masks, and auto-textarea promotion for observations in public pre-matricula
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m1s Details

This commit is contained in:
Sidney 2026-05-28 09:35:54 -03:00
parent 9c1e604cb9
commit b97a15544c
4 changed files with 92 additions and 12 deletions

View File

@ -244,6 +244,30 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
<span className="text-xs font-mono text-slate-500">{config?.corPrimaria}</span> <span className="text-xs font-mono text-slate-500">{config?.corPrimaria}</span>
</div> </div>
</div> </div>
<div className="space-y-2">
<label className="block text-[10px] font-bold text-slate-500 uppercase">Campos Padrão do Formulário</label>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3 space-y-2">
<label className="flex items-center gap-2 text-xs font-bold text-slate-700 cursor-pointer">
<input
type="checkbox"
className="w-4 h-4 text-indigo-600 rounded border-slate-300 focus:ring-indigo-500"
checked={config?.exibirEmail !== false}
onChange={(e) => setConfig(prev => prev ? { ...prev, exibirEmail: e.target.checked } : prev)}
/>
Exibir Campo E-mail
</label>
<label className="flex items-center gap-2 text-xs font-bold text-slate-700 cursor-pointer">
<input
type="checkbox"
className="w-4 h-4 text-indigo-600 rounded border-slate-300 focus:ring-indigo-500"
checked={config?.exibirTelefone !== false}
onChange={(e) => setConfig(prev => prev ? { ...prev, exibirTelefone: e.target.checked } : prev)}
/>
Exibir Campo Telefone / WhatsApp
</label>
</div>
<p className="text-[10px] text-slate-400 font-medium">Nome Completo é sempre obrigatório para identificar o interessado.</p>
</div>
<div> <div>
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-2">Turmas Disponíveis no Formulário</label> <label className="block text-[10px] font-bold text-slate-500 uppercase mb-2">Turmas Disponíveis no Formulário</label>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3 max-h-40 overflow-y-auto space-y-1"> <div className="bg-slate-50 border border-slate-200 rounded-lg p-3 max-h-40 overflow-y-auto space-y-1">

View File

@ -9,11 +9,18 @@ async function run() {
try { try {
await client.query(` await client.query(`
ALTER TABLE prematricula_config ALTER TABLE prematricula_config
ADD COLUMN IF NOT EXISTS turmas_permitidas TEXT[] DEFAULT '{}'; ADD COLUMN IF NOT EXISTS exibir_email BOOLEAN DEFAULT TRUE;
ALTER TABLE prematricula_config
ADD COLUMN IF NOT EXISTS exibir_telefone BOOLEAN DEFAULT TRUE;
UPDATE prematricula_config
SET exibir_email = TRUE, exibir_telefone = TRUE
WHERE exibir_email IS NULL OR exibir_telefone IS NULL;
`); `);
console.log('✅ Coluna turmas_permitidas adicionada com sucesso!'); console.log('✅ Colunas exibir_email e exibir_telefone adicionadas com sucesso!');
} catch (e) { } catch (e) {
console.error('❌ Erro ao adicionar coluna:', e.message); console.error('❌ Erro ao adicionar colunas:', e.message);
} finally { } finally {
client.release(); client.release();
await pool.end(); await pool.end();

View File

@ -108,6 +108,38 @@ body{font-family:'Inter',sans-serif;background:#f8fafc;min-height:100vh;display:
<script> <script>
const slug='${slug}'; const slug='${slug}';
const app=document.getElementById('app'); const app=document.getElementById('app');
function maskPhone(v){
if(!v)return "";
v=v.replace(/\D/g,"");
v=v.replace(/(\d{2})(\d)/,"($1) $2");
v=v.replace(/(\d{5})(\d)/,"$1-$2");
return v.substring(0,15);
}
function maskCPF(v){
if(!v)return "";
v=v.replace(/\D/g,"");
v=v.replace(/(\d{3})(\d)/,"$1.$2");
v=v.replace(/(\d{3})(\d)/,"$1.$2");
v=v.replace(/(\d{3})(\d{1,2})$/,"$1-$2");
return v.substring(0,14);
}
function maskCEP(v){
if(!v)return "";
v=v.replace(/\D/g,"");
v=v.replace(/(\d{5})(\d)/,"$1-$2");
return v.substring(0,9);
}
document.addEventListener('input',function(e){
if(!e.target)return;
const dt=e.target.getAttribute('data-type');
if(dt==='phone'){
e.target.value=maskPhone(e.target.value);
}else if(dt==='cpf'){
e.target.value=maskCPF(e.target.value);
}else if(dt==='cep'){
e.target.value=maskCEP(e.target.value);
}
});
async function init(){ async function init(){
try{ try{
const res=await fetch('/api/prematricula/public/'+slug); const res=await fetch('/api/prematricula/public/'+slug);
@ -120,8 +152,12 @@ async function init(){
html+='<h1>'+c.titulo+'</h1><p>'+c.descricao+'</p></div>'; html+='<h1>'+c.titulo+'</h1><p>'+c.descricao+'</p></div>';
html+='<form class="form" id="preForm" onsubmit="return submitForm(event)">'; html+='<form class="form" id="preForm" onsubmit="return submitForm(event)">';
html+='<div class="field"><label>Nome Completo <span class="req">*</span></label><input name="_nome" required placeholder="Seu nome completo"></div>'; html+='<div class="field"><label>Nome Completo <span class="req">*</span></label><input name="_nome" required placeholder="Seu nome completo"></div>';
html+='<div class="field"><label>Email</label><input name="_email" type="email" placeholder="seu@email.com"></div>'; if(c.exibirEmail !== false){
html+='<div class="field"><label>Telefone / WhatsApp</label><input name="_telefone" placeholder="(00) 00000-0000"></div>'; html+='<div class="field"><label>Email</label><input name="_email" type="email" placeholder="seu@email.com"></div>';
}
if(c.exibirTelefone !== false){
html+='<div class="field"><label>Telefone / WhatsApp</label><input name="_telefone" data-type="phone" placeholder="(00) 00000-0000"></div>';
}
if(data.turmas.length>0){ if(data.turmas.length>0){
html+='<div class="field"><label>Turma de Interesse <span class="req">*</span></label><select name="_turma" required><option value="">Selecione a turma...</option>'; html+='<div class="field"><label>Turma de Interesse <span class="req">*</span></label><select name="_turma" required><option value="">Selecione a turma...</option>';
data.turmas.forEach(t=>{html+='<option value="'+t.id+'" data-nome="'+t.nome+'">'+t.nome+'</option>'}); data.turmas.forEach(t=>{html+='<option value="'+t.id+'" data-nome="'+t.nome+'">'+t.nome+'</option>'});
@ -129,15 +165,17 @@ async function init(){
} }
data.campos.forEach(f=>{ data.campos.forEach(f=>{
html+='<div class="field"><label>'+f.label+(f.obrigatorio?' <span class="req">*</span>':'')+'</label>'; html+='<div class="field"><label>'+f.label+(f.obrigatorio?' <span class="req">*</span>':'')+'</label>';
const isTextarea = f.tipo === 'textarea' || (f.label && /observa/i.test(f.label));
if(f.tipo==='select'){ if(f.tipo==='select'){
html+='<select name="'+f.id+'"'+(f.obrigatorio?' required':'')+'><option value="">Selecione...</option>'; html+='<select name="'+f.id+'"'+(f.obrigatorio?' required':'')+'><option value="">Selecione...</option>';
(f.opcoes||[]).forEach(o=>{html+='<option value="'+o+'">'+o+'</option>'}); (f.opcoes||[]).forEach(o=>{html+='<option value="'+o+'">'+o+'</option>'});
html+='</select>'; html+='</select>';
}else if(f.tipo==='textarea'){ }else if(isTextarea){
html+='<textarea name="'+f.id+'"'+(f.obrigatorio?' required':'')+' placeholder="'+(f.placeholder||'')+'"></textarea>'; html+='<textarea name="'+f.id+'"'+(f.obrigatorio?' required':'')+' placeholder="'+(f.placeholder||'')+'"></textarea>';
}else{ }else{
let t=f.tipo;if(t==='phone'||t==='cpf')t='text'; let t=f.tipo;if(t==='phone'||t==='cpf'||t==='cep')t='text';
html+='<input type="'+t+'" name="'+f.id+'"'+(f.obrigatorio?' required':'')+' placeholder="'+(f.placeholder||'')+'">'; let dt=(f.tipo==='phone'||f.tipo==='cpf'||f.tipo==='cep')?(' data-type="'+f.tipo+'"'):'';
html+='<input type="'+t+'" name="'+f.id+'"'+(f.obrigatorio?' required':'')+dt+' placeholder="'+(f.placeholder||'')+'">';
} }
html+='</div>'; html+='</div>';
}); });
@ -2898,7 +2936,9 @@ async function startServer() {
id: r.id, titulo: r.titulo, descricao: r.descricao, slug: r.slug, id: r.id, titulo: r.titulo, descricao: r.descricao, slug: r.slug,
status: r.status, corPrimaria: r.cor_primaria, logoUrl: r.logo_url, status: r.status, corPrimaria: r.cor_primaria, logoUrl: r.logo_url,
mensagemSucesso: r.mensagem_sucesso, mensagemSucesso: r.mensagem_sucesso,
turmasPermitidas: r.turmas_permitidas || [] turmasPermitidas: r.turmas_permitidas || [],
exibirEmail: r.exibir_email !== false,
exibirTelefone: r.exibir_telefone !== false
}}); }});
} catch (e) { console.error(e); res.status(500).json({ error: e.message }); } } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
}); });
@ -2908,8 +2948,13 @@ async function startServer() {
const c = req.body; const c = req.body;
await pool.query( await pool.query(
`UPDATE prematricula_config SET titulo=$1, descricao=$2, slug=$3, status=$4, `UPDATE prematricula_config SET titulo=$1, descricao=$2, slug=$3, status=$4,
cor_primaria=$5, logo_url=$6, mensagem_sucesso=$7, turmas_permitidas=$8, updated_at=NOW() WHERE id=1`, cor_primaria=$5, logo_url=$6, mensagem_sucesso=$7, turmas_permitidas=$8,
[c.titulo, c.descricao, c.slug, c.status, c.corPrimaria || '#4f46e5', c.logoUrl || '', c.mensagemSucesso || '', c.turmasPermitidas || []] exibir_email=$9, exibir_telefone=$10, updated_at=NOW() WHERE id=1`,
[
c.titulo, c.descricao, c.slug, c.status, c.corPrimaria || '#4f46e5',
c.logoUrl || '', c.mensagemSucesso || '', c.turmasPermitidas || [],
c.exibirEmail !== false, c.exibirTelefone !== false
]
); );
res.json({ success: true }); res.json({ success: true });
} catch (e) { console.error(e); res.status(500).json({ error: e.message }); } } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
@ -3014,7 +3059,9 @@ async function startServer() {
res.json({ res.json({
config: { config: {
titulo: cfg.titulo, descricao: cfg.descricao, corPrimaria: cfg.cor_primaria, titulo: cfg.titulo, descricao: cfg.descricao, corPrimaria: cfg.cor_primaria,
logoUrl: cfg.logo_url, mensagemSucesso: cfg.mensagem_sucesso logoUrl: cfg.logo_url, mensagemSucesso: cfg.mensagem_sucesso,
exibirEmail: cfg.exibir_email !== false,
exibirTelefone: cfg.exibir_telefone !== false
}, },
campos: camposRows.map(r => ({ campos: camposRows.map(r => ({
id: r.id, label: r.label, tipo: r.tipo, placeholder: r.placeholder, id: r.id, label: r.label, tipo: r.tipo, placeholder: r.placeholder,

View File

@ -279,6 +279,8 @@ export interface PreMatriculaConfig {
logoUrl: string; logoUrl: string;
mensagemSucesso: string; mensagemSucesso: string;
turmasPermitidas?: string[]; turmasPermitidas?: string[];
exibirEmail?: boolean;
exibirTelefone?: boolean;
} }
export interface PreMatriculaInscricao { export interface PreMatriculaInscricao {