diff --git a/act_runner_config.yaml b/act_runner_config.yaml new file mode 100644 index 0000000..a5fb814 --- /dev/null +++ b/act_runner_config.yaml @@ -0,0 +1,4 @@ +runner: + insecure: false +container: + network: "network_public" diff --git a/config_gitea.sh b/config_gitea.sh new file mode 100644 index 0000000..d78c48b --- /dev/null +++ b/config_gitea.sh @@ -0,0 +1,4 @@ +echo "" | sudo tee -a /var/lib/docker/volumes/fa03e1d7b997cab51d36d907dab232d316fcda69b9bc3a3e356c8d97bd4a6ace/_data/app.ini +echo "[actions]" | sudo tee -a /var/lib/docker/volumes/fa03e1d7b997cab51d36d907dab232d316fcda69b9bc3a3e356c8d97bd4a6ace/_data/app.ini +echo "ENABLED = true" | sudo tee -a /var/lib/docker/volumes/fa03e1d7b997cab51d36d907dab232d316fcda69b9bc3a3e356c8d97bd4a6ace/_data/app.ini +docker restart $(docker ps -q -f name=gitea_gitea-server) diff --git a/create_config.py b/create_config.py new file mode 100644 index 0000000..bc7caf1 --- /dev/null +++ b/create_config.py @@ -0,0 +1,3 @@ +import yaml +with open('/home/ubuntu/act_runner_config.yaml', 'w') as f: + f.write('runner:\n insecure: false\ncontainer:\n network: "network_public"\n') diff --git a/manager/components/Classes.tsx b/manager/components/Classes.tsx index 4518dec..6fccc5a 100644 --- a/manager/components/Classes.tsx +++ b/manager/components/Classes.tsx @@ -303,8 +303,12 @@ const Classes: React.FC = ({ data, updateData, onNavigateToClass } const calculateAge = (birthDate: string) => { if (!birthDate) return null; + const cleanDate = birthDate.substring(0, 10); + if (!cleanDate.includes('-')) return null; + const [year, month, day] = cleanDate.split('-').map(Number); + const birth = new Date(year, month - 1, day); const today = new Date(); - const birth = new Date(birthDate); + let age = today.getFullYear() - birth.getFullYear(); const m = today.getMonth() - birth.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) { diff --git a/manager/components/Dashboard.tsx b/manager/components/Dashboard.tsx index b46582a..f18ac14 100644 --- a/manager/components/Dashboard.tsx +++ b/manager/components/Dashboard.tsx @@ -436,9 +436,9 @@ const Dashboard: React.FC = ({ data }) => { s.gender === 'M').length }, - { name: 'Feminino', value: data.students.filter(s => s.gender === 'F').length }, - { name: 'Outro', value: data.students.filter(s => s.gender === 'O').length }, + { name: 'Feminino', value: data.students.filter(s => (s as any).sexo === 'Feminino' || s.gender === 'F' || (s as any).sexo === 'F').length }, + { name: 'Masculino', value: data.students.filter(s => (s as any).sexo === 'Masculino' || s.gender === 'M' || (s as any).sexo === 'M').length }, + { name: 'Outro', value: data.students.filter(s => (s as any).sexo === 'Outro' || s.gender === 'O' || (s as any).sexo === 'O').length }, ]} cx="50%" cy="50%" @@ -446,8 +446,8 @@ const Dashboard: React.FC = ({ data }) => { dataKey="value" label > - + diff --git a/manager/components/Students.tsx b/manager/components/Students.tsx index d1292a7..e681709 100644 --- a/manager/components/Students.tsx +++ b/manager/components/Students.tsx @@ -106,7 +106,8 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId hasGuardian: false, contractTemplateId: '', generateFee: false, // UI only - generateContract: false // UI only + generateContract: false, // UI only + sexo: '' } as any); // Camera State @@ -279,8 +280,10 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId }; const calculateAge = (dateString: string) => { - if (!dateString || !dateString.includes('-')) return null; - const [year, month, day] = dateString.split('-').map(Number); + if (!dateString) return null; + const cleanDate = dateString.substring(0, 10); + if (!cleanDate.includes('-')) return null; + const [year, month, day] = cleanDate.split('-').map(Number); const birthDate = new Date(year, month - 1, day); const today = new Date(); @@ -318,11 +321,12 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId useEffect(() => { if (formData.birthDate) { - const parts = formData.birthDate.split('-'); + const cleanDate = formData.birthDate.substring(0, 10); + const parts = cleanDate.split('-'); if (parts.length === 3) { setBirthDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`); } else { - setBirthDateInput(formData.birthDate); + setBirthDateInput(cleanDate); } } else { setBirthDateInput(''); @@ -331,11 +335,12 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId useEffect(() => { if (formData.guardianBirthDate) { - const parts = formData.guardianBirthDate.split('-'); + const cleanDate = formData.guardianBirthDate.substring(0, 10); + const parts = cleanDate.split('-'); if (parts.length === 3) { setGuardianBirthDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`); } else { - setGuardianBirthDateInput(formData.guardianBirthDate); + setGuardianBirthDateInput(cleanDate); } } else { setGuardianBirthDateInput(''); @@ -344,11 +349,12 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId useEffect(() => { if (formData.rgIssueDate) { - const parts = formData.rgIssueDate.split('-'); + const cleanDate = formData.rgIssueDate.substring(0, 10); + const parts = cleanDate.split('-'); if (parts.length === 3) { setRgIssueDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`); } else { - setRgIssueDateInput(formData.rgIssueDate); + setRgIssueDateInput(cleanDate); } } else { setRgIssueDateInput(''); @@ -839,6 +845,9 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId const studentToSave: Student = { ...(editingStudent || { id: studentId }), ...formData as Student, + birthDate: formData.birthDate ? formData.birthDate.substring(0, 10) : '', + guardianBirthDate: formData.guardianBirthDate ? formData.guardianBirthDate.substring(0, 10) : '', + rgIssueDate: formData.rgIssueDate ? formData.rgIssueDate.substring(0, 10) : '', enrollmentNumber, portalPassword, photo: finalPhotoUrl @@ -925,7 +934,7 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId content = content.replace(/{{aluno}}/g, studentToSave.name || ''); content = content.replace(/{{aluno_cpf}}/g, studentToSave.cpf || ''); content = content.replace(/{{aluno_rg}}/g, studentToSave.rg || ''); - content = content.replace(/{{aluno_nascimento}}/g, studentToSave.birthDate ? studentToSave.birthDate.split('-').reverse().join('/') : ''); + content = content.replace(/{{aluno_nascimento}}/g, studentToSave.birthDate ? studentToSave.birthDate.substring(0, 10).split('-').reverse().join('/') : ''); content = content.replace(/{{aluno_email}}/g, studentToSave.email || ''); content = content.replace(/{{aluno_telefone}}/g, studentToSave.phone || ''); content = content.replace(/{{aluno_cep}}/g, studentToSave.addressZip || ''); @@ -937,7 +946,7 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId // Responsável content = content.replace(/{{responsavel_nome}}/g, studentToSave.guardianName || ''); content = content.replace(/{{responsavel_cpf}}/g, studentToSave.guardianCpf || ''); - content = content.replace(/{{responsavel_nascimento}}/g, studentToSave.guardianBirthDate ? studentToSave.guardianBirthDate.split('-').reverse().join('/') : ''); + content = content.replace(/{{responsavel_nascimento}}/g, studentToSave.guardianBirthDate ? studentToSave.guardianBirthDate.substring(0, 10).split('-').reverse().join('/') : ''); // Curso e Turma content = content.replace(/{{curso}}/g, course.name || ''); @@ -1186,7 +1195,8 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId discount: 0, hasGuardian: false, generateFee: true, - generateContract: true + generateContract: true, + sexo: '' }; if (student) { @@ -1602,14 +1612,29 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId Dados Pessoais -
- - setFormData({...formData, name: e.target.value})} - placeholder="Ex: João da Silva" - /> +
+
+ + setFormData({...formData, name: e.target.value})} + placeholder="Ex: João da Silva" + /> +
+
+ + +
diff --git a/manager/fix_all_dates.cjs b/manager/fix_all_dates.cjs new file mode 100644 index 0000000..daf1062 --- /dev/null +++ b/manager/fix_all_dates.cjs @@ -0,0 +1,19 @@ +const { Pool } = require('pg'); +const pool = new Pool({ connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' }); +async function fixAll() { + const client = await pool.connect(); + try { + const { rowCount } = await client.query(` + UPDATE frequencias + SET data = data - INTERVAL '3 hours' + WHERE EXTRACT(HOUR FROM data) >= 17 + AND EXTRACT(HOUR FROM data) <= 19 + AND tipo = 'presence' + `); + console.log(`Corrigidas ${rowCount} presenças deslocadas pelo fuso horário (UTC -> BRT).`); + } finally { + client.release(); + pool.end(); + } +} +fixAll().catch(console.error); diff --git a/manager/fix_dates.cjs b/manager/fix_dates.cjs new file mode 100644 index 0000000..4ca3bf3 --- /dev/null +++ b/manager/fix_dates.cjs @@ -0,0 +1,14 @@ +const { Pool } = require('pg'); +const pool = new Pool({ connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' }); +async function fix() { + const client = await pool.connect(); + try { + await client.query("UPDATE frequencias SET data = '2026-05-02 14:37:10' WHERE id = 'b1961ef4-9a35-495a-86dc-4e6439d9670b'"); + await client.query("UPDATE frequencias SET data = '2026-04-25 15:10:55' WHERE id = '810ac2f3-860b-4173-a3c9-203c45f3d061'"); + console.log('Fixed dates for Napoleão'); + } finally { + client.release(); + pool.end(); + } +} +fix().catch(console.error); diff --git a/manager/scratch/add_sexo_column.cjs b/manager/scratch/add_sexo_column.cjs new file mode 100644 index 0000000..5508402 --- /dev/null +++ b/manager/scratch/add_sexo_column.cjs @@ -0,0 +1,16 @@ +const { Pool } = require('pg'); +const pool = new Pool({ connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' }); + +async function run() { + try { + console.log('Adicionando coluna "sexo" na tabela "alunos"...'); + await pool.query('ALTER TABLE alunos ADD COLUMN IF NOT EXISTS sexo text;'); + console.log('Coluna "sexo" adicionada ou já existente com sucesso!'); + } catch (err) { + console.error('Erro ao adicionar coluna:', err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/check_frequencias_photos.js b/manager/scratch/check_frequencias_photos.js new file mode 100644 index 0000000..56bfcd2 --- /dev/null +++ b/manager/scratch/check_frequencias_photos.js @@ -0,0 +1,30 @@ +import pg from 'pg'; + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +async function run() { + try { + console.log('Buscando presenças com fotos na tabela frequencias...'); + const { rows } = await pool.query( + `SELECT f.aluno_id, a.nome, count(*) as total, array_agg(f.data ORDER BY f.data ASC) as datas + FROM frequencias f + JOIN alunos a ON f.aluno_id = a.id + WHERE f.foto IS NOT NULL AND f.foto <> '' + GROUP BY f.aluno_id, a.nome` + ); + + console.log(`Encontrados ${rows.length} alunos com fotos de presenças.`); + rows.forEach(r => { + console.log(`Aluno: ${r.nome} (ID: ${r.aluno_id}) | Total Presenças com Foto: ${r.total} | Datas: ${r.datas.join(', ')}`); + }); + + } catch (err) { + console.error(err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/check_json_keys.js b/manager/scratch/check_json_keys.js new file mode 100644 index 0000000..03abf6c --- /dev/null +++ b/manager/scratch/check_json_keys.js @@ -0,0 +1,33 @@ +import pg from 'pg'; + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +async function run() { + try { + const { rows } = await pool.query('SELECT data FROM school_data WHERE id = 1'); + const data = rows[0]?.data; + if (!data) { + console.log('No data found.'); + return; + } + + console.log('--- CHAVES NO JSONB ---'); + for (const key of Object.keys(data)) { + if (Array.isArray(data[key])) { + console.log(`- ${key}: Array com ${data[key].length} itens`); + } else if (typeof data[key] === 'object' && data[key] !== null) { + console.log(`- ${key}: Objeto com chaves [${Object.keys(data[key]).join(', ')}]`); + } else { + console.log(`- ${key}: ${typeof data[key]}`); + } + } + } catch (err) { + console.error(err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/compare_students.js b/manager/scratch/compare_students.js new file mode 100644 index 0000000..5901b3e --- /dev/null +++ b/manager/scratch/compare_students.js @@ -0,0 +1,31 @@ +import pg from 'pg'; + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +async function run() { + try { + const { rows: schoolDataRows } = await pool.query("SELECT data->'students' as students FROM school_data WHERE id = 1"); + const jsonStudents = schoolDataRows[0]?.students || []; + + const { rows: sqlStudents } = await pool.query("SELECT id, nome, foto_url, face_descriptor, data_nascimento, cpf FROM alunos"); + + console.log('--- ALUNOS NO JSONB (school_data) ---'); + jsonStudents.forEach(s => { + console.log(`ID: ${s.id} | Nome: ${s.name} | Photo: ${s.photo} | BirthDate: ${s.birthDate} | CPF: ${s.cpf} | HasFaceDescriptor: ${!!s.faceDescriptor}`); + }); + + console.log('\n--- ALUNOS NA TABELA SQL (alunos) ---'); + sqlStudents.forEach(s => { + console.log(`ID: ${s.id} | Nome: ${s.nome} | FotoUrl: ${s.foto_url} | BirthDate: ${s.data_nascimento} | CPF: ${s.cpf} | HasFaceDescriptor: ${!!s.face_descriptor}`); + }); + + } catch (err) { + console.error(err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/delete_orphaned_absences.cjs b/manager/scratch/delete_orphaned_absences.cjs new file mode 100644 index 0000000..08ba56f --- /dev/null +++ b/manager/scratch/delete_orphaned_absences.cjs @@ -0,0 +1,29 @@ +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 { + const res = await client.query(` + DELETE FROM frequencias a + WHERE a.tipo = 'absence' AND a.id LIKE 'auto-abs-%' + AND EXISTS ( + SELECT 1 FROM frequencias p + WHERE p.tipo = 'presence' + AND p.aluno_id = a.aluno_id + AND (p.aula_id = a.aula_id OR DATE(p.data AT TIME ZONE 'UTC') = DATE(a.data AT TIME ZONE 'UTC')) + ) RETURNING id; + `); + console.log(`Deleted ${res.rowCount} orphaned absences.`); + } catch (err) { + console.error(err); + } finally { + client.release(); + pool.end(); + } +} + +run(); diff --git a/manager/scratch/delete_orphaned_absences.js b/manager/scratch/delete_orphaned_absences.js new file mode 100644 index 0000000..0815ba8 --- /dev/null +++ b/manager/scratch/delete_orphaned_absences.js @@ -0,0 +1,29 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@postgres:5432/edumanager' +}); + +async function run() { + const client = await pool.connect(); + try { + const res = await client.query(` + DELETE FROM frequencias a + WHERE a.tipo = 'absence' AND a.id LIKE 'auto-abs-%' + AND EXISTS ( + SELECT 1 FROM frequencias p + WHERE p.tipo = 'presence' + AND p.aluno_id = a.aluno_id + AND (p.aula_id = a.aula_id OR DATE(p.data AT TIME ZONE 'UTC') = DATE(a.data AT TIME ZONE 'UTC')) + ) RETURNING id; + `); + console.log(`Deleted ${res.rowCount} orphaned absences.`); + } catch (err) { + console.error(err); + } finally { + client.release(); + pool.end(); + } +} + +run(); diff --git a/manager/scratch/delete_orphaned_absences2.cjs b/manager/scratch/delete_orphaned_absences2.cjs new file mode 100644 index 0000000..b0d6de1 --- /dev/null +++ b/manager/scratch/delete_orphaned_absences2.cjs @@ -0,0 +1,27 @@ +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 { + const res = await client.query(` + DELETE FROM frequencias a + WHERE a.tipo = 'absence' AND a.id LIKE 'auto-abs-%' + AND NOT EXISTS ( + SELECT 1 FROM school_data sd + WHERE jsonb_path_exists(sd.data, ('$.attendance[*] ? (@.id == "' || a.id || '")')::jsonpath) + ) RETURNING id; + `); + console.log(`Deleted ${res.rowCount} absences that are in SQL but NOT in JSON.`); + } catch (err) { + console.error(err); + } finally { + client.release(); + pool.end(); + } +} + +run(); diff --git a/manager/scratch/download_photos.js b/manager/scratch/download_photos.js new file mode 100644 index 0000000..8f1d30d --- /dev/null +++ b/manager/scratch/download_photos.js @@ -0,0 +1,61 @@ +import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const s3 = new S3Client({ + endpoint: 'https://storageedu.microtecinformaticacurso.com.br:443', + region: 'us-east-1', + credentials: { + accessKeyId: 'minioadmin', + secretAccessKey: 'MiniO2026!Seguro' + }, + forcePathStyle: true, + tls: false // Disable SSL verification for self-signed certs +}); + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +async function run() { + const destDir = path.join(__dirname, 'photos'); + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + try { + console.log('Listando objetos no bucket fotos-alunos...'); + const listCommand = new ListObjectsV2Command({ + Bucket: 'fotos-alunos' + }); + const listResponse = await s3.send(listCommand); + const objects = listResponse.Contents || []; + console.log(`Encontrados ${objects.length} objetos.`); + + for (const obj of objects) { + if (!obj.Key) continue; + console.log(`Baixando ${obj.Key}...`); + const getCommand = new GetObjectCommand({ + Bucket: 'fotos-alunos', + Key: obj.Key + }); + const getResponse = await s3.send(getCommand); + const stream = getResponse.Body; + if (stream) { + const destPath = path.join(destDir, obj.Key); + const fileStream = fs.createWriteStream(destPath); + // Convert web stream / readable stream to local file + const buffer = await stream.transformToByteArray(); + fs.writeFileSync(destPath, buffer); + console.log(`Salvo em ${destPath}`); + } + } + console.log('Download concluído!'); + } catch (err) { + console.error('Erro:', err); + } +} + +run(); diff --git a/manager/scratch/fix_face.cjs b/manager/scratch/fix_face.cjs new file mode 100644 index 0000000..c941865 --- /dev/null +++ b/manager/scratch/fix_face.cjs @@ -0,0 +1,16 @@ +const { Pool } = require('pg'); +const pool = new Pool({ connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' }); +async function run() { + const { rows } = await pool.query('SELECT data FROM school_data LIMIT 1'); + const students = rows[0].data.students || []; + let updated = 0; + for (const s of students) { + if (s.faceDescriptor && Array.isArray(s.faceDescriptor)) { + await pool.query('UPDATE alunos SET face_descriptor = $1 WHERE id = $2', [JSON.stringify(s.faceDescriptor), s.id]); + updated++; + } + } + console.log('Total alunos atualizados com face_descriptor:', updated); + await pool.end(); +} +run().catch(console.error); diff --git a/manager/scratch/inspect_jsonb.js b/manager/scratch/inspect_jsonb.js new file mode 100644 index 0000000..bcf4b0f --- /dev/null +++ b/manager/scratch/inspect_jsonb.js @@ -0,0 +1,52 @@ +import pg from 'pg'; + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +async function run() { + try { + console.log('Buscando school_data...'); + const { rows } = await pool.query('SELECT data FROM school_data WHERE id = 1'); + const data = rows[0]?.data; + if (!data) { + console.log('school_data não encontrado.'); + return; + } + + // Recursively search for keys or values containing "webp" or "fotos-alunos" + const foundPaths = []; + function search(obj, path = '') { + if (!obj) return; + if (typeof obj === 'string') { + if (obj.includes('webp') || obj.includes('fotos-alunos')) { + foundPaths.push({ path, value: obj }); + } + } else if (Array.isArray(obj)) { + obj.forEach((item, index) => search(item, `${path}[${index}]`)); + } else if (typeof obj === 'object') { + for (const key of Object.keys(obj)) { + search(obj[key], `${path}.${key}`); + } + } + } + + search(data); + + console.log('\n--- LINKS ENCONTRADOS NO JSONB ---'); + if (foundPaths.length === 0) { + console.log('Nenhum link encontrado.'); + } else { + foundPaths.forEach(p => { + console.log(`Caminho: ${p.path} | Valor: ${p.value}`); + }); + } + + } catch (err) { + console.error(err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/match_faces.js b/manager/scratch/match_faces.js new file mode 100644 index 0000000..5b3d1b9 --- /dev/null +++ b/manager/scratch/match_faces.js @@ -0,0 +1,117 @@ +import * as faceapi from '@vladmandic/face-api'; +import sharp from 'sharp'; +import pg from 'pg'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Disable certificate verification for CDN download if needed +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +async function run() { + try { + // 1. Initialize face-api environment for Node + console.log('Inicializando face-api...'); + + // Tiny Face Detector, Face Landmark 68, and Face Recognition Nets + const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/'; + console.log('Carregando modelos face-api do CDN...'); + await Promise.all([ + faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL), + faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL), + faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL) + ]); + console.log('Modelos carregados com sucesso!'); + + // 2. Fetch students and their face descriptors from DB + console.log('Buscando alunos do banco de dados...'); + const { rows: students } = await pool.query('SELECT id, nome, face_descriptor FROM alunos WHERE face_descriptor IS NOT NULL'); + console.log(`Encontrados ${students.length} alunos com biometria no banco.`); + + // 3. Read downloaded photos from disk + const photosDir = path.join(__dirname, 'photos'); + const photoFiles = fs.readdirSync(photosDir).filter(f => f.endsWith('.webp')); + console.log(`Encontradas ${photoFiles.length} fotos webp locais.`); + + const results = []; + + // 4. Process each photo, get its descriptor, and find best match + for (const file of photoFiles) { + const filePath = path.join(photosDir, file); + console.log(`Processando ${file}...`); + + // Use sharp to convert webp to raw RGB buffer for face-api tensor + const image = sharp(filePath); + const metadata = await image.metadata(); + const { data, info } = await image + .raw() + .toBuffer({ resolveWithObject: true }); + + // Create tensor from raw pixels + const tensor = faceapi.tf.tensor3d( + new Uint8Array(data), + [info.height, info.width, 3], + 'int32' + ); + + // Detect face and extract descriptor + const detection = await faceapi.detectSingleFace(tensor) + .withFaceLandmarks() + .withFaceDescriptor(); + + tensor.dispose(); + + if (!detection) { + console.warn(`Nenhum rosto detectado na foto ${file}`); + continue; + } + + const imgDescriptor = detection.descriptor; + + // Compare with all students using Euclidean distance + let bestMatch = null; + let minDistance = Infinity; + + for (const s of students) { + const dbDescriptorStr = typeof s.face_descriptor === 'string' + ? s.face_descriptor + : JSON.stringify(s.face_descriptor); + + const dbDescriptor = new Float32Array(JSON.parse(dbDescriptorStr)); + const distance = faceapi.euclideanDistance(imgDescriptor, dbDescriptor); + + if (distance < minDistance) { + minDistance = distance; + bestMatch = s; + } + } + + console.log(`Foto: ${file} | Melhor Match: ${bestMatch?.nome} | Distância: ${minDistance.toFixed(4)}`); + results.push({ + file, + studentId: bestMatch?.id, + studentName: bestMatch?.nome, + distance: minDistance + }); + } + + console.log('\n--- RESULTADO DE PROJEÇÃO DE CORRELAÇÃO DE BIOMETRIA ---'); + results.forEach(r => { + console.log(`Foto: ${r.file} -> Aluno: ${r.studentName} (ID: ${r.studentId}) | Distância: ${r.distance.toFixed(4)}`); + }); + + } catch (err) { + console.error('Erro na execução:', err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/photos/student_1776870119652_z0tdgp.webp b/manager/scratch/photos/student_1776870119652_z0tdgp.webp new file mode 100644 index 0000000..6737ede Binary files /dev/null and b/manager/scratch/photos/student_1776870119652_z0tdgp.webp differ diff --git a/manager/scratch/photos/student_1777139875689_4l971l.webp b/manager/scratch/photos/student_1777139875689_4l971l.webp new file mode 100644 index 0000000..e2decd3 Binary files /dev/null and b/manager/scratch/photos/student_1777139875689_4l971l.webp differ diff --git a/manager/scratch/photos/student_1777139911929_21v6av.webp b/manager/scratch/photos/student_1777139911929_21v6av.webp new file mode 100644 index 0000000..8466232 Binary files /dev/null and b/manager/scratch/photos/student_1777139911929_21v6av.webp differ diff --git a/manager/scratch/photos/student_1777139942925_aoxyls.webp b/manager/scratch/photos/student_1777139942925_aoxyls.webp new file mode 100644 index 0000000..ba3ee68 Binary files /dev/null and b/manager/scratch/photos/student_1777139942925_aoxyls.webp differ diff --git a/manager/scratch/photos/student_1777140008401_3966cd.webp b/manager/scratch/photos/student_1777140008401_3966cd.webp new file mode 100644 index 0000000..c89f76f Binary files /dev/null and b/manager/scratch/photos/student_1777140008401_3966cd.webp differ diff --git a/manager/scratch/photos/student_1777140038192_xntcja.webp b/manager/scratch/photos/student_1777140038192_xntcja.webp new file mode 100644 index 0000000..a3d999a Binary files /dev/null and b/manager/scratch/photos/student_1777140038192_xntcja.webp differ diff --git a/manager/scratch/photos/student_1777140066701_dfzecd.webp b/manager/scratch/photos/student_1777140066701_dfzecd.webp new file mode 100644 index 0000000..6fdc115 Binary files /dev/null and b/manager/scratch/photos/student_1777140066701_dfzecd.webp differ diff --git a/manager/scratch/photos/student_1777140098875_tcan7k.webp b/manager/scratch/photos/student_1777140098875_tcan7k.webp new file mode 100644 index 0000000..986222c Binary files /dev/null and b/manager/scratch/photos/student_1777140098875_tcan7k.webp differ diff --git a/manager/scratch/photos/student_1777743393187_rf50t.webp b/manager/scratch/photos/student_1777743393187_rf50t.webp new file mode 100644 index 0000000..0c32cc2 Binary files /dev/null and b/manager/scratch/photos/student_1777743393187_rf50t.webp differ diff --git a/manager/scratch/restore_and_link_students.js b/manager/scratch/restore_and_link_students.js new file mode 100644 index 0000000..21f3e35 --- /dev/null +++ b/manager/scratch/restore_and_link_students.js @@ -0,0 +1,176 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import pg from 'pg'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +const photoMapping = { + // 1. ANA CLARA DA SILVA NASCIMENTO (17:57:55) + 'e917ffe5-70d6-451e-9120-7692ec8d7024': '/storage/fotos-alunos/student_1777139875689_4l971l.webp', + + // 2. ANTÔNIA CERMILANE PEREIRA PINHEIRO (17:58:31) + 'b9a7ddee-e03f-411f-9ef2-d22629d38e35': '/storage/fotos-alunos/student_1777139911929_21v6av.webp', + + // 3. DOUGLAS EMANUEL DE SOUSA PEREIRA (17:59:02) + '0ab24299-f583-40eb-8812-d6005ebc50a8': '/storage/fotos-alunos/student_1777139942925_aoxyls.webp', + + // 4. EVILLA PINHEIRO DA SILVA BORGES (18:00:08) + '311709fb-68ab-4168-8684-887b5ec2d731': '/storage/fotos-alunos/student_1777140008401_3966cd.webp', + + // 5. GABRIELY CAETANO DA SILVA (18:00:38) + '0ef75207-3ab1-4524-b737-b543e804d3f0': '/storage/fotos-alunos/student_1777140038192_xntcja.webp', + + // 6. KARLA BIANCA PINHEIRO TRINDADE (18:01:06) + 'c7e2d021-52ab-4229-8550-1aa8549507b3': '/storage/fotos-alunos/student_1777140066701_dfzecd.webp', + + // 7. NAPOLEÃO DA SILVA CARDOSO (18:01:38) + '3653aea3-7e7e-49d6-b372-068863084a27': '/storage/fotos-alunos/student_1777140098875_tcan7k.webp', + + // 8. Sidney Gomes da silva (15:01) + '5a231b04-b95c-4026-ba37-bc7b8144f646': '/storage/fotos-alunos/student_1776870119652_z0tdgp.webp', + + // 9. MARIA LOHANNY ROQUE DA SILVA (May 2) + '05de4757-d36f-4de6-9e57-5792d13ad3e7': '/storage/fotos-alunos/student_1777743393187_rf50t.webp' +}; + +async function run() { + const backupPath = path.join(__dirname, '../backup_supabase_2026-04-19.json'); + console.log(`Lendo dados completos do backup original de: ${backupPath}`); + + if (!fs.existsSync(backupPath)) { + console.error('ERRO: Arquivo de backup original não encontrado!'); + process.exit(1); + } + + const raw = fs.readFileSync(backupPath, 'utf8'); + const backupData = JSON.parse(raw); + const backupStudents = backupData.students || []; + + try { + // 1. Atualizar registros individuais na tabela alunos + console.log('Atualizando a tabela "alunos" no PostgreSQL...'); + for (const bs of backupStudents) { + const mappedPhoto = photoMapping[bs.id]; + if (!mappedPhoto) continue; + + const birthDate = bs.birthDate ? bs.birthDate.split('T')[0] : null; + const rgIssueDate = bs.rgIssueDate ? bs.rgIssueDate.split('T')[0] : null; + const guardianBirthDate = bs.guardianBirthDate ? bs.guardianBirthDate.split('T')[0] : null; + const registrationDate = bs.registrationDate ? bs.registrationDate.split('T')[0] : null; + + const query = ` + UPDATE alunos + SET + rg = $1, + data_nascimento = $2, + rg_data_emissao = $3, + cep = $4, + rua = $5, + numero = $6, + bairro = $7, + cidade = $8, + estado = $9, + tem_responsavel = $10, + nome_responsavel = $11, + cpf_responsavel = $12, + telefone_responsavel = $13, + data_nascimento_responsavel = $14, + numero_matricula = $15, + data_matricula = $16, + modelo_contrato_id = $17, + foto_url = $18, + senha_portal = $19 + WHERE id = $20 + `; + + const values = [ + bs.rg || '', + birthDate, + rgIssueDate, + bs.addressZip || '', + bs.addressStreet || '', + bs.addressNumber || '', + bs.addressNeighborhood || '', + bs.addressCity || '', + bs.addressState || '', + bs.hasGuardian || false, + bs.guardianName || '', + bs.guardianCpf || '', + bs.guardianPhone || '', + guardianBirthDate, + bs.enrollmentNumber || null, + registrationDate, + bs.contractTemplateId || 'default-template', + mappedPhoto, + bs.portalPassword || null, + bs.id + ]; + + await pool.query(query, values); + console.log(`✅ [SQL] Restaurado e Vinculado: ${bs.name} (${bs.id}) -> ${mappedPhoto}`); + } + + // 2. Buscar, atualizar e salvar o espelho JSONB em school_data + console.log('\nSincronizando espelho JSONB na tabela "school_data"...'); + const { rows } = await pool.query('SELECT data FROM school_data WHERE id = 1'); + const schoolData = rows[0]?.data; + + if (!schoolData) { + console.error('ERRO: Registro do school_data com ID=1 não encontrado!'); + return; + } + + let updatedJsonCount = 0; + if (schoolData.students && Array.isArray(schoolData.students)) { + for (const s of schoolData.students) { + const bs = backupStudents.find(x => x.id === s.id); + const mappedPhoto = photoMapping[s.id]; + + if (bs && mappedPhoto) { + s.photo = mappedPhoto; + s.foto_url = mappedPhoto; + s.birthDate = bs.birthDate; + s.rg = bs.rg || ''; + s.rgIssueDate = bs.rgIssueDate; + s.addressZip = bs.addressZip || ''; + s.addressStreet = bs.addressStreet || ''; + s.addressNumber = bs.addressNumber || ''; + s.addressNeighborhood = bs.addressNeighborhood || ''; + s.addressCity = bs.addressCity || ''; + s.addressState = bs.addressState || ''; + s.hasGuardian = bs.hasGuardian || false; + s.guardianName = bs.guardianName || ''; + s.guardianCpf = bs.guardianCpf || ''; + s.guardianPhone = bs.guardianPhone || ''; + s.guardianBirthDate = bs.guardianBirthDate; + s.enrollmentNumber = bs.enrollmentNumber || null; + s.registrationDate = bs.registrationDate; + s.portalPassword = bs.portalPassword || null; + + updatedJsonCount++; + console.log(`✅ [JSONB] Atualizado no mirror: ${s.name} (${s.id})`); + } + } + + // Salvar de volta ao banco de dados usando COMMIT para persistir a gravação em modo leitura/gravação + await pool.query('COMMIT'); + await pool.query('BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED'); + await pool.query('UPDATE school_data SET data = $1 WHERE id = 1', [JSON.stringify(schoolData)]); + await pool.query('COMMIT'); + console.log(`\n🎉 Sincronização concluída! ${updatedJsonCount} registros atualizados com sucesso no JSONB.`); + } + + } catch (err) { + console.error('ERRO CRÍTICO DURANTE A RESTAURAÇÃO:', err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/restore_students.js b/manager/scratch/restore_students.js new file mode 100644 index 0000000..810f996 --- /dev/null +++ b/manager/scratch/restore_students.js @@ -0,0 +1,137 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import pg from 'pg'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Try local first (with SSH tunnel), then remote directly +const URLS = [ + 'postgresql://edumanager:EduManager2026!Seguro@127.0.0.1:5432/edumanager', + 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +]; + +async function run() { + const backupPath = path.join(__dirname, '../backup_supabase_2026-04-19_migrado.json'); + console.log(`Lendo backup de: ${backupPath}`); + + if (!fs.existsSync(backupPath)) { + console.error('ERRO: Arquivo de backup não encontrado!'); + process.exit(1); + } + + const raw = fs.readFileSync(backupPath, 'utf8'); + const backupData = JSON.parse(raw); + const students = backupData.students || []; + + console.log(`Encontrados ${students.length} alunos no arquivo de backup.`); + + let pool; + for (const url of URLS) { + try { + console.log(`Tentando conectar ao banco via: ${url.split('@')[1]}...`); + pool = new pg.Pool({ connectionString: url, connectionTimeoutMillis: 5000 }); + await pool.query('SELECT NOW()'); + console.log('Conectado com sucesso!'); + break; + } catch (err) { + console.warn(`Falha ao conectar via ${url.split('@')[1]}: ${err.message}`); + pool = null; + } + } + + if (!pool) { + console.error('ERRO: Não foi possível conectar ao banco de dados com nenhuma das URLs.'); + process.exit(1); + } + + let updatedCount = 0; + let notFoundCount = 0; + + try { + for (const s of students) { + // Formatar datas para o padrão YYYY-MM-DD + const birthDate = s.birthDate ? s.birthDate.split('T')[0] : null; + const rgIssueDate = s.rgIssueDate ? s.rgIssueDate.split('T')[0] : null; + const guardianBirthDate = s.guardianBirthDate ? s.guardianBirthDate.split('T')[0] : null; + const registrationDate = s.registrationDate ? s.registrationDate.split('T')[0] : null; + + // Verificar se o aluno existe + const { rows } = await pool.query('SELECT id, nome FROM alunos WHERE id = $1', [s.id]); + + if (rows.length === 0) { + console.log(`Aluno não encontrado no banco SQL (Será ignorado): ${s.name} (${s.id})`); + notFoundCount++; + continue; + } + + // Executar o UPDATE dos dados restaurados + const query = ` + UPDATE alunos + SET + rg = $1, + data_nascimento = $2, + rg_data_emissao = $3, + cep = $4, + rua = $5, + numero = $6, + bairro = $7, + cidade = $8, + estado = $9, + tem_responsavel = $10, + nome_responsavel = $11, + cpf_responsavel = $12, + telefone_responsavel = $13, + data_nascimento_responsavel = $14, + numero_matricula = $15, + data_matricula = $16, + modelo_contrato_id = $17, + foto_url = $18, + senha_portal = $19, + face_descriptor = $20 + WHERE id = $21 + `; + + const values = [ + s.rg || '', + birthDate, + rgIssueDate, + s.addressZip || '', + s.addressStreet || '', + s.addressNumber || '', + s.addressNeighborhood || '', + s.addressCity || '', + s.addressState || '', + s.hasGuardian || false, + s.guardianName || '', + s.guardianCpf || '', + s.guardianPhone || '', + guardianBirthDate, + s.enrollmentNumber || null, + registrationDate, + s.contractTemplateId || 'default-template', + s.photo || '', + s.portalPassword || null, + s.faceDescriptor ? JSON.stringify(s.faceDescriptor) : null, + s.id + ]; + + await pool.query(query, values); + console.log(`Restaurado aluno: ${s.name} (${s.id})`); + updatedCount++; + } + + console.log('\n--- RELATÓRIO DE RESTAURAÇÃO ---'); + console.log(`Alunos atualizados com sucesso no SQL: ${updatedCount}`); + console.log(`Alunos do backup não encontrados no SQL: ${notFoundCount}`); + console.log('--------------------------------\n'); + + } catch (err) { + console.error('ERRO CRÍTICO DURANTE A RESTAURAÇÃO:', err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/search_db.js b/manager/scratch/search_db.js new file mode 100644 index 0000000..364de09 --- /dev/null +++ b/manager/scratch/search_db.js @@ -0,0 +1,64 @@ +import pg from 'pg'; + +const pool = new pg.Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' +}); + +const files = [ + 'student_1777139875689_4l971l.webp', + 'student_1777139911929_21v6av.webp', + 'student_1777139942925_aoxyls.webp', + 'student_1777140008401_3966cd.webp', + 'student_1777140038192_xntcja.webp', + 'student_1777140066701_dfzecd.webp', + 'student_1777140098875_tcan7k.webp', + 'student_1777743393187_rf50t.webp' +]; + +async function run() { + try { + const { rows: tables } = await pool.query( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'" + ); + + console.log(`Buscando em ${tables.length} tabelas no banco...`); + + for (const t of tables) { + const tableName = t.table_name; + const { rows: cols } = await pool.query( + "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1", + [tableName] + ); + + for (const c of cols) { + if (['text', 'character varying', 'jsonb'].includes(c.data_type)) { + for (const f of files) { + try { + const queryStr = `SELECT id FROM ${tableName} WHERE "${c.column_name}"::text LIKE $1`; + const { rows: matches } = await pool.query(queryStr, [`%${f}%`]); + if (matches.length > 0) { + console.log(`🔍 [ENCONTRADO] Arquivo ${f} na tabela [${tableName}], coluna [${c.column_name}]! IDs:`, matches.map(m => m.id)); + } + } catch (e) { + // Ignorar erros de colunas sem coluna 'id' + try { + const queryStr = `SELECT count(*) FROM ${tableName} WHERE "${c.column_name}"::text LIKE $1`; + const { rows: matches } = await pool.query(queryStr, [`%${f}%`]); + if (parseInt(matches[0].count) > 0) { + console.log(`🔍 [ENCONTRADO] Arquivo ${f} na tabela [${tableName}], coluna [${c.column_name}] (Count: ${matches[0].count})`); + } + } catch (e2) {} + } + } + } + } + } + console.log('Busca finalizada.'); + } catch (err) { + console.error('Erro na busca:', err); + } finally { + await pool.end(); + } +} + +run(); diff --git a/manager/scratch/update_alunos_db.cjs b/manager/scratch/update_alunos_db.cjs new file mode 100644 index 0000000..c42f6e4 --- /dev/null +++ b/manager/scratch/update_alunos_db.cjs @@ -0,0 +1,36 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: 'postgresql://edumanager:EduManager2026!Seguro@localhost:5432/edumanager' +}); + +async function run() { + try { + const res = await pool.query(` + UPDATE alunos a + SET + data_nascimento = (s.elem->>'data_nascimento')::timestamp, + rg = s.elem->>'rg', + rua = s.elem->>'rua', + numero = s.elem->>'numero', + bairro = s.elem->>'bairro', + cidade = s.elem->>'cidade', + estado = s.elem->>'estado', + cep = s.elem->>'cep' + FROM ( + SELECT jsonb_array_elements(data->'students') as elem + FROM school_data + WHERE id = 1 + ) s + WHERE a.id = s.elem->>'id' + RETURNING a.nome; + `); + console.log(`Sucesso! ${res.rowCount} alunos atualizados diretamente no PostgreSQL.`); + } catch (error) { + console.error('Erro na migração:', error); + } finally { + pool.end(); + } +} + +run(); diff --git a/manager/scratch/update_gemini.js b/manager/scratch/update_gemini.js new file mode 100644 index 0000000..d74d541 --- /dev/null +++ b/manager/scratch/update_gemini.js @@ -0,0 +1,18 @@ +import fs from 'fs'; + +const filePath = '../GEMINI.md'; +let content = fs.readFileSync(filePath, 'utf-8'); + +const newLine = "44. **Biometria SQL-First Completa**: A biometria (faceDescriptor) e todo o fluxo de cadastro e manipulação de alunos foram migrados para persistência nativa 100% SQL-First no PostgreSQL via API `/api/alunos`. O frontend `Students.tsx` foi totalmente refatorado, eliminando dependências de escrita direta em `school_data.json` no cliente, enquanto o backend executa a sincronização reversa automática para compatibilidade."; + +// Find any line starting with "4 4 . * * B i o m e t r i a" +const lines = content.split(/\r?\n/); +const updatedLines = lines.map(line => { + if (line.includes("4 4 . * * B i o m e t r i a")) { + return newLine; + } + return line; +}); + +fs.writeFileSync(filePath, updatedLines.join('\n'), 'utf-8'); +console.log("GEMINI.md updated successfully!"); diff --git a/manager/services/database.js b/manager/services/database.js index 5b217b7..ac85fd4 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -641,6 +641,20 @@ export async function deleteCategoriaFuncionario(id) { // ============================================================ // ALUNOS (FASE 4) // ============================================================ +function formatDateOnly(val) { + if (!val) return null; + if (val instanceof Date) { + const year = val.getFullYear(); + const month = String(val.getMonth() + 1).padStart(2, '0'); + const day = String(val.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + if (typeof val === 'string') { + return val.substring(0, 10); + } + return String(val).substring(0, 10); +} + export async function getAlunos() { const result = await pool.query("SELECT * FROM alunos ORDER BY nome ASC"); return result.rows.map(r => ({ @@ -649,17 +663,17 @@ export async function getAlunos() { name: r.nome, email: r.email, phone: r.telefone, - birthDate: r.data_nascimento, + birthDate: formatDateOnly(r.data_nascimento), cpf: r.cpf, rg: r.rg, - rgIssueDate: r.rg_data_emissao, + rgIssueDate: formatDateOnly(r.rg_data_emissao), guardianName: r.nome_responsavel, guardianPhone: r.telefone_responsavel, guardianCpf: r.cpf_responsavel, - guardianBirthDate: r.data_nascimento_responsavel, + guardianBirthDate: formatDateOnly(r.data_nascimento_responsavel), classId: r.turma_id, status: r.status, - registrationDate: r.data_matricula, + registrationDate: formatDateOnly(r.data_matricula), photo: r.foto_url, addressZip: r.cep, addressStreet: r.rua, @@ -673,59 +687,72 @@ export async function getAlunos() { enrollmentNumber: r.numero_matricula, portalPassword: r.senha_portal, cancellationReason: r.motivo_cancelamento, - faceDescriptor: r.face_descriptor + faceDescriptor: r.face_descriptor, + sexo: r.sexo })); } export async function insertAluno(a) { + const birthDate = (a.data_nascimento || a.birthDate) ? formatDateOnly(a.data_nascimento || a.birthDate) : null; + const rgIssueDate = (a.rg_data_emissao || a.rgIssueDate) ? formatDateOnly(a.rg_data_emissao || a.rgIssueDate) : null; + const guardianBirthDate = (a.data_nascimento_responsavel || a.guardianBirthDate) ? formatDateOnly(a.data_nascimento_responsavel || a.guardianBirthDate) : null; + const registrationDate = (a.data_matricula || a.registrationDate) ? formatDateOnly(a.data_matricula || a.registrationDate) : null; + const result = await pool.query( `INSERT INTO alunos ( id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao, nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel, turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado, desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal, - motivo_cancelamento, face_descriptor + motivo_cancelamento, face_descriptor, sexo ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30 ) RETURNING *`, [ - a.id, a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null, - a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null, + a.id, a.nome || a.name, a.email || '', a.telefone || a.phone || '', birthDate, + a.cpf || '', a.rg || '', rgIssueDate, a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '', - a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null, - a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null, - a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '', - a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '', - a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false), - a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null, - a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null, - a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : null - ] - ); - return result.rows[0]; -} - -export async function updateAluno(id, a) { - const result = await pool.query( - `UPDATE alunos SET - nome=$1, email=$2, telefone=$3, data_nascimento=$4, cpf=$5, rg=$6, rg_data_emissao=$7, - nome_responsavel=$8, telefone_responsavel=$9, cpf_responsavel=$10, data_nascimento_responsavel=$11, - turma_id=$12, status=$13, data_matricula=$14, foto_url=$15, cep=$16, rua=$17, numero=$18, bairro=$19, cidade=$20, estado=$21, - desconto=$22, tem_responsavel=$23, modelo_contrato_id=$24, numero_matricula=$25, senha_portal=$26, - motivo_cancelamento=$27, face_descriptor=COALESCE($28, face_descriptor) - WHERE id = $29 RETURNING *`, - [ - a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null, - a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null, - a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '', - a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null, - a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null, + a.cpf_responsavel || a.guardianCpf || '', guardianBirthDate, + a.turma_id || a.classId || null, a.status || 'active', registrationDate, a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '', a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '', a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false), a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null, a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null, a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : null, + a.sexo || null + ] + ); + return result.rows[0]; +} + +export async function updateAluno(id, a) { + const birthDate = (a.data_nascimento || a.birthDate) ? formatDateOnly(a.data_nascimento || a.birthDate) : null; + const rgIssueDate = (a.rg_data_emissao || a.rgIssueDate) ? formatDateOnly(a.rg_data_emissao || a.rgIssueDate) : null; + const guardianBirthDate = (a.data_nascimento_responsavel || a.guardianBirthDate) ? formatDateOnly(a.data_nascimento_responsavel || a.guardianBirthDate) : null; + const registrationDate = (a.data_matricula || a.registrationDate) ? formatDateOnly(a.data_matricula || a.registrationDate) : null; + + const result = await pool.query( + `UPDATE alunos SET + nome=$1, email=$2, telefone=$3, data_nascimento=$4, cpf=$5, rg=$6, rg_data_emissao=$7, + nome_responsavel=$8, telefone_responsavel=$9, cpf_responsavel=$10, data_nascimento_responsavel=$11, + turma_id=$12, status=$13, data_matricula=$14, foto_url=$15, cep=$16, rua=$17, numero=$18, bairro=$19, cidade=$20, estado=$21, + desconto=$22, tem_responsavel=$23, modelo_contrato_id=$24, numero_matricula=$25, senha_portal=$26, + motivo_cancelamento=$27, face_descriptor=COALESCE($28, face_descriptor), sexo=$29 + WHERE id = $30 RETURNING *`, + [ + a.nome || a.name, a.email || '', a.telefone || a.phone || '', birthDate, + a.cpf || '', a.rg || '', rgIssueDate, + a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '', + a.cpf_responsavel || a.guardianCpf || '', guardianBirthDate, + a.turma_id || a.classId || null, a.status || 'active', registrationDate, + a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '', + a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '', + a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false), + a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null, + a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null, + a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : null, + a.sexo || null, id ] ); @@ -1146,8 +1173,8 @@ export async function syncJsonToRelationalTables() { id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao, nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel, turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado, - desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal, face_descriptor - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28) + desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal, face_descriptor, sexo + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29) ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, email = EXCLUDED.email, telefone = EXCLUDED.telefone, data_nascimento = EXCLUDED.data_nascimento, cpf = EXCLUDED.cpf, rg = EXCLUDED.rg, rg_data_emissao = EXCLUDED.rg_data_emissao, @@ -1158,14 +1185,16 @@ export async function syncJsonToRelationalTables() { bairro = EXCLUDED.bairro, cidade = EXCLUDED.cidade, estado = EXCLUDED.estado, desconto = EXCLUDED.desconto, tem_responsavel = EXCLUDED.tem_responsavel, modelo_contrato_id = EXCLUDED.modelo_contrato_id, numero_matricula = EXCLUDED.numero_matricula, - senha_portal = EXCLUDED.senha_portal, face_descriptor = COALESCE(EXCLUDED.face_descriptor, alunos.face_descriptor)`, + senha_portal = EXCLUDED.senha_portal, face_descriptor = COALESCE(EXCLUDED.face_descriptor, alunos.face_descriptor), + sexo = EXCLUDED.sexo`, [ s.id, s.name || s.nome, s.email || '', s.phone || s.telefone || '', s.birthDate || s.data_nascimento || null, s.cpf || '', s.rg || '', s.rgIssueDate || s.rg_data_emissao || null, s.guardianName || s.nome_responsavel || '', s.guardianPhone || s.telefone_responsavel || '', s.guardianCpf || s.cpf_responsavel || '', s.guardianBirthDate || s.data_nascimento_responsavel || null, s.classId || s.turma_id || null, s.status || 'active', s.registrationDate || s.data_matricula || null, s.photo || s.foto_url || '', s.addressZip || s.cep || '', s.addressStreet || s.rua || '', s.addressNumber || s.numero || '', s.addressNeighborhood || s.bairro || '', s.addressCity || s.cidade || '', s.addressState || s.estado || '', s.discount || s.desconto || 0, s.hasGuardian !== undefined ? s.hasGuardian : (s.tem_responsavel || false), s.contractTemplateId || s.modelo_contrato_id || null, s.enrollmentNumber || s.numero_matricula || null, s.portalPassword || s.senha_portal || null, - s.faceDescriptor ? JSON.stringify(s.faceDescriptor) : null + s.faceDescriptor ? JSON.stringify(s.faceDescriptor) : null, + s.sexo || null ] ); } diff --git a/manager/tsconfig.json b/manager/tsconfig.json index 2c6eed5..bc371ca 100644 --- a/manager/tsconfig.json +++ b/manager/tsconfig.json @@ -25,5 +25,10 @@ }, "allowImportingTsExtensions": true, "noEmit": true - } + }, + "exclude": [ + "node_modules", + "scratch", + "dist" + ] } \ No newline at end of file diff --git a/manager/types.ts b/manager/types.ts index 93ee069..7b2db39 100644 --- a/manager/types.ts +++ b/manager/types.ts @@ -51,6 +51,7 @@ export interface Student { contractTemplateId?: string; // Vínculo com o modelo de contrato enrollmentNumber?: string; // Número de matrícula (login do portal do aluno) portalPassword?: string; // Senha do portal do aluno (padrão: 6 primeiros dígitos do CPF) + sexo?: string; } export interface Class { diff --git a/portal/src/pages/Financeiro.tsx b/portal/src/pages/Financeiro.tsx index e77a463..0df5b03 100644 --- a/portal/src/pages/Financeiro.tsx +++ b/portal/src/pages/Financeiro.tsx @@ -387,7 +387,7 @@ export default function Financeiro() {
Valor Pago: - {formatCurrency(getEffectiveValue(receiptPayment))} + {formatCurrency(getDisplayValue(receiptPayment))}
Data de Vencimento: diff --git a/portal/src/pages/Frequencia.tsx b/portal/src/pages/Frequencia.tsx index de9b8ba..5743380 100644 --- a/portal/src/pages/Frequencia.tsx +++ b/portal/src/pages/Frequencia.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useAuth } from '../context/AuthContext'; -import { CalendarCheck, CheckCircle2, XCircle, FileText, Send, X, Loader2, AlertTriangle, ChevronDown, Clock } from 'lucide-react'; +import { CalendarCheck, CheckCircle2, XCircle, FileText, Send, X, Loader2, AlertTriangle, ChevronDown, Clock, AlertCircle } from 'lucide-react'; import type { Attendance, Lesson } from '../types'; import { getLessonTimeStatus, getNormalizedDate, isLessonWithinJustificationWindow, parseLessonDateTime } from '../lib/lessonUtils'; import { useRealTimeDate } from '../hooks/useRealTimeDate';