From b1f723ce88b3ac5b1f7f540e7f69e8ff5793358b Mon Sep 17 00:00:00 2001 From: Sidney Date: Thu, 28 May 2026 19:23:23 -0300 Subject: [PATCH] feat: pre-enrollment capacity limits, unified draggable fields list editor, public form logo scale-up, and executive dashboard analytics integration --- manager/components/Dashboard.tsx | 135 ++++++- manager/components/PreMatricula.tsx | 331 ++++++++++++++---- .../update_vagas_and_default_fields.cjs | 40 +++ manager/server.selfhosted.js | 195 +++++++++-- manager/services/database.js | 67 ---- 5 files changed, 601 insertions(+), 167 deletions(-) create mode 100644 manager/scratch/update_vagas_and_default_fields.cjs diff --git a/manager/components/Dashboard.tsx b/manager/components/Dashboard.tsx index f18ac14..1320ae8 100644 --- a/manager/components/Dashboard.tsx +++ b/manager/components/Dashboard.tsx @@ -30,7 +30,8 @@ import { AlertCircle, Calendar, ChevronRight, - Layout + Layout, + ClipboardPen } from 'lucide-react'; import { pdfService } from '../services/pdfService'; @@ -42,6 +43,7 @@ const Dashboard: React.FC = ({ data }) => { const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [dashboardView, setDashboardView] = useState<'standard' | 'detailed'>('standard'); const [dbClasses, setDbClasses] = useState(data.classes || []); + const [prematriculas, setPrematriculas] = useState([]); React.useEffect(() => { fetch('/api/turmas') @@ -57,6 +59,15 @@ const Dashboard: React.FC = ({ data }) => { } }) .catch(console.error); + + fetch('/api/prematricula/inscricoes') + .then(res => res.json()) + .then(json => { + if (json.inscricoes) { + setPrematriculas(json.inscricoes); + } + }) + .catch(console.error); }, []); // Basic Stats @@ -163,6 +174,14 @@ const Dashboard: React.FC = ({ data }) => { const stats = [ { label: 'Alunos Ativos', value: activeStudents, icon: Users, color: 'text-blue-600', bg: 'bg-blue-100', trend: '+12%' }, { label: 'Turmas Ativas', value: totalClasses, icon: BookOpen, color: 'text-indigo-600', bg: 'bg-indigo-100', trend: '+2' }, + { + label: 'Pré-Matrículas', + value: prematriculas.length, + icon: ClipboardPen, + color: 'text-amber-600', + bg: 'bg-amber-100', + trend: `${prematriculas.filter(p => p.status === 'pendente').length} pendentes` + }, { label: 'Receita Total', value: `R$ ${revenue.toLocaleString()}`, icon: Wallet, color: 'text-emerald-600', bg: 'bg-emerald-100', trend: '+8.4%' }, { label: 'Taxa de Presença', value: `${attendanceRate}%`, icon: TrendingUp, color: 'text-purple-600', bg: 'bg-purple-100', trend: '+2.1%' }, ]; @@ -212,7 +231,7 @@ const Dashboard: React.FC = ({ data }) => { {/* Main Stats Grid */} -
+
{stats.map((stat, i) => (
@@ -426,6 +445,118 @@ const Dashboard: React.FC = ({ data }) => {
+ {/* Pré-Matrículas Section */} +
+ {/* Recent Pre-Enrollments */} +
+
+
+

Pré-Matrículas Recentes

+

Últimas solicitações recebidas

+
+ + {prematriculas.filter(p => p.status === 'pendente').length} Pendentes + +
+ +
+ {prematriculas.length > 0 ? ( + + + + + + + + + + + + {prematriculas.slice(0, 5).map((p) => { + const statusColor = + p.status === 'aprovado' ? 'bg-emerald-100 text-emerald-800 font-bold' : + p.status === 'rejeitado' ? 'bg-rose-100 text-rose-800 font-bold' : + 'bg-amber-100 text-amber-800 font-bold'; + + return ( + + + + + + + + ); + })} + +
CandidatoContatoTurma de InteresseStatusData
{p.nome}{p.telefone || p.email || '—'}{p.turmaNome || 'Geral'} + + {p.status} + + + {new Date(p.createdAt || p.created_at).toLocaleDateString('pt-BR')} +
+ ) : ( +
Nenhuma pré-matrícula recebida
+ )} +
+
+ + {/* Pre-Enrollment Stats Funnel */} +
+

Funil de Pré-Matrícula

+

Conversão e status

+ +
+
+
+

Total Geral

+

{prematriculas.length}

+
+
+ Σ +
+
+ +
+
+

Aguardando Análise

+

+ {prematriculas.filter(p => p.status === 'pendente').length} +

+
+ + ⏰ + +
+ +
+
+

Aprovados

+

+ {prematriculas.filter(p => p.status === 'aprovado').length} +

+
+ + ✓ + +
+ +
+
+

Rejeitados

+

+ {prematriculas.filter(p => p.status === 'rejeitado').length} +

+
+ + ✕ + +
+
+
+
+ {/* Detailed View Expansion */} {dashboardView === 'detailed' && (
diff --git a/manager/components/PreMatricula.tsx b/manager/components/PreMatricula.tsx index 24ee544..817d8d2 100644 --- a/manager/components/PreMatricula.tsx +++ b/manager/components/PreMatricula.tsx @@ -3,7 +3,7 @@ import { SchoolData, PreMatriculaCampo, PreMatriculaConfig, PreMatriculaInscrica import { Plus, Trash2, Save, Eye, EyeOff, Download, GripVertical, Link2, ClipboardPen, Copy, Check, ChevronDown, ChevronRight, Users, RefreshCw, - FileText, X, Settings2, ArrowLeft, ExternalLink, AlertCircle, UserCheck + FileText, X, Settings2, ArrowLeft, ExternalLink, AlertCircle, UserCheck, Edit } from 'lucide-react'; interface Props { @@ -116,6 +116,31 @@ const PreMatricula: React.FC = ({ data, onConvert }) => { } catch (e) { console.error(e); } }; + const moveField = async (index: number, direction: 'up' | 'down') => { + const targetIndex = direction === 'up' ? index - 1 : index + 1; + if (targetIndex < 0 || targetIndex >= campos.length) return; + + const reordered = [...campos].sort((a, b) => a.ordem - b.ordem); + const temp = reordered[index]; + reordered[index] = reordered[targetIndex]; + reordered[targetIndex] = temp; + + const updated = reordered.map((c, i) => ({ ...c, ordem: i })); + setCampos(updated); + + try { + await fetch('/api/prematricula/campos/reordenar', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ordem: updated.map(c => ({ id: c.id, ordem: c.ordem })) + }) + }); + } catch (e) { + console.error(e); + } + }; + const deleteInscricao = async (id: string) => { try { const res = await fetch(`/api/prematricula/inscricoes/${id}`, { method: 'DELETE' }); @@ -244,58 +269,119 @@ const PreMatricula: React.FC = ({ data, onConvert }) => { {config?.corPrimaria}
-
- -
- - -
-

Nome Completo é sempre obrigatório para identificar o interessado.

-
-
- {turmas.map(t => { - const isChecked = config?.turmasPermitidas?.includes(t.id); + + {/* Selected Classes Chips with grey Tags/Chips UI */} +
+ {(config?.turmasPermitidas || []).map(id => { + const turma = turmas.find(t => t.id === id); + if (!turma) return null; return ( -
-

Se nenhuma for marcada, todas as turmas aparecerão por padrão.

+ + {/* Dropdown to add a class */} +
+ +
+ +
+
+ + {/* Configurar Vagas e Horários por Turma */} + {config?.turmasPermitidas && config.turmasPermitidas.length > 0 && ( +
+ +
+ {config.turmasPermitidas.map(id => { + const turma = turmas.find(t => t.id === id); + if (!turma) return null; + const horarioStr = turma.horario || `${turma.dia_semana || ''} ${turma.horario_inicio_padrao || ''} - ${turma.horario_fim_padrao || ''}`.trim() || 'Horário não definido'; + return ( +
+
+

{turma.nome}

+

+ Horário: {horarioStr} +

+
+
+ Vagas: + { + const val = e.target.value === '' ? null : parseInt(e.target.value); + setTurmas(prev => prev.map(t => t.id === id ? { ...t, vagas_prematricula: val } : t)); + }} + onBlur={async (e) => { + const val = e.target.value === '' ? null : parseInt(e.target.value); + try { + await fetch(`/api/turmas/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ vagas_prematricula: val }) + }); + } catch (err) { + console.error('Erro ao atualizar vagas:', err); + } + }} + /> +
+
+ ); + })} +
+
+ )} + +

+ 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). +

@@ -389,27 +475,150 @@ const PreMatricula: React.FC = ({ data, onConvert }) => {

Clique em "Novo Campo" para começar a montar seu formulário.

)} - {campos.sort((a, b) => a.ordem - b.ordem).map((campo, idx) => ( -
- -
-
-

{campo.label}

- {campo.obrigatorio && *} + {campos.sort((a, b) => a.ordem - b.ordem).map((campo, idx) => { + const isDefaultField = campo.id.startsWith('_'); + return ( +
+ {/* Move Up/Down Controls for premium reordering */} +
+ + +
+ +
+
+

{campo.label}

+ {campo.obrigatorio && *} + {isDefaultField && ( + + Padrão + + )} +
+

+ {FIELD_TYPES[campo.tipo] || campo.tipo} + {campo.tipo === 'select' && campo.opcoes?.length ? ` · ${campo.opcoes.length} opções` : ''} +

+
+ +
+ + + {campo.id !== '_nome' && ( + + )}
-

- {FIELD_TYPES[campo.tipo] || campo.tipo} - {campo.tipo === 'select' && campo.opcoes?.length ? ` · ${campo.opcoes.length} opções` : ''} -

- -
- ))} + ); + })}
+ + {/* Floating Edit Field Modal - Premium Floating Principle (Rule 11 & 17) */} + {editingCampo && ( +
+
setEditingCampo(null)} /> +
+ + +

Editar Campo

+ +
+
+ + setEditingCampo({ ...editingCampo, label: e.target.value })} + /> +
+ +
+ + +
+ +
+ + setEditingCampo({ ...editingCampo, placeholder: e.target.value })} + /> +
+ + {editingCampo.tipo === 'select' && ( +
+ + setEditingCampo({ ...editingCampo, opcoes: e.target.value.split(',').map(x => x.trim()) })} + /> +
+ )} + + +
+ +
+ + +
+
+
+ )}
diff --git a/manager/scratch/update_vagas_and_default_fields.cjs b/manager/scratch/update_vagas_and_default_fields.cjs new file mode 100644 index 0000000..ef65a54 --- /dev/null +++ b/manager/scratch/update_vagas_and_default_fields.cjs @@ -0,0 +1,40 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +async function run() { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // 1. Add vagas_prematricula to turmas + await client.query(` + ALTER TABLE turmas ADD COLUMN IF NOT EXISTS vagas_prematricula INTEGER DEFAULT NULL; + `); + console.log('✅ Coluna vagas_prematricula adicionada à tabela turmas!'); + + // 2. Insert default fields in prematricula_campos + await client.query(` + INSERT INTO prematricula_campos (id, label, tipo, placeholder, obrigatorio, opcoes, ordem, ativo) + VALUES + ('_nome', 'Nome Completo', 'text', 'Seu nome completo', true, '{}', 10, true), + ('_email', 'Email', 'email', 'seu@email.com', false, '{}', 20, true), + ('_telefone', 'Telefone / WhatsApp', 'phone', '(00) 00000-0000', false, '{}', 30, true), + ('_turma', 'Turma de Interesse', 'select', 'Selecione a turma...', true, '{}', 40, true) + ON CONFLICT (id) DO NOTHING; + `); + console.log('✅ Campos padrão inseridos na tabela prematricula_campos!'); + + await client.query('COMMIT'); + } catch (e) { + await client.query('ROLLBACK'); + console.error('❌ Erro na migração:', e.message); + } finally { + client.release(); + await pool.end(); + } +} + +run(); diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index c7ca654..30f062a 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -79,7 +79,7 @@ function getPreMatriculaHTML(slug) { body{font-family:'Inter',sans-serif;background:#f8fafc;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px} .container{max-width:640px;width:100%;background:#fff;border-radius:24px;box-shadow:0 25px 50px -12px rgba(0,0,0,.1);overflow:hidden} .header{padding:40px 32px 24px;text-align:center} -.header img{max-height:56px;margin:0 auto 16px} +.header img{max-height:110px;margin:0 auto 24px;display:block} .header h1{font-size:28px;font-weight:900;color:#0f172a;margin-bottom:8px} .header p{font-size:14px;color:#64748b;line-height:1.6} .form{padding:0 32px 32px} @@ -108,27 +108,44 @@ body{font-family:'Inter',sans-serif;background:#f8fafc;min-height:100vh;display: