feat: add student sexo field and update detailed dashboard
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m18s Details

This commit is contained in:
Sidney 2026-05-27 11:37:26 -03:00
parent 470eb3163e
commit 81ba8b1919
38 changed files with 1049 additions and 69 deletions

4
act_runner_config.yaml Normal file
View File

@ -0,0 +1,4 @@
runner:
insecure: false
container:
network: "network_public"

4
config_gitea.sh Normal file
View File

@ -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)

3
create_config.py Normal file
View File

@ -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')

View File

@ -303,8 +303,12 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
const calculateAge = (birthDate: string) => { const calculateAge = (birthDate: string) => {
if (!birthDate) return null; 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 today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear(); let age = today.getFullYear() - birth.getFullYear();
const m = today.getMonth() - birth.getMonth(); const m = today.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) { if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) {

View File

@ -436,9 +436,9 @@ const Dashboard: React.FC<DashboardProps> = ({ data }) => {
<PieChart> <PieChart>
<Pie <Pie
data={[ data={[
{ name: 'Masculino', value: data.students.filter(s => s.gender === 'M').length }, { name: 'Feminino', value: data.students.filter(s => (s as any).sexo === 'Feminino' || s.gender === 'F' || (s as any).sexo === 'F').length },
{ name: 'Feminino', value: data.students.filter(s => s.gender === '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.gender === 'O').length }, { name: 'Outro', value: data.students.filter(s => (s as any).sexo === 'Outro' || s.gender === 'O' || (s as any).sexo === 'O').length },
]} ]}
cx="50%" cx="50%"
cy="50%" cy="50%"
@ -446,8 +446,8 @@ const Dashboard: React.FC<DashboardProps> = ({ data }) => {
dataKey="value" dataKey="value"
label label
> >
<Cell fill="#3b82f6" />
<Cell fill="#ec4899" /> <Cell fill="#ec4899" />
<Cell fill="#3b82f6" />
<Cell fill="#94a3b8" /> <Cell fill="#94a3b8" />
</Pie> </Pie>
<Tooltip /> <Tooltip />

View File

@ -106,7 +106,8 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
hasGuardian: false, hasGuardian: false,
contractTemplateId: '', contractTemplateId: '',
generateFee: false, // UI only generateFee: false, // UI only
generateContract: false // UI only generateContract: false, // UI only
sexo: ''
} as any); } as any);
// Camera State // Camera State
@ -279,8 +280,10 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
}; };
const calculateAge = (dateString: string) => { const calculateAge = (dateString: string) => {
if (!dateString || !dateString.includes('-')) return null; if (!dateString) return null;
const [year, month, day] = dateString.split('-').map(Number); 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 birthDate = new Date(year, month - 1, day);
const today = new Date(); const today = new Date();
@ -318,11 +321,12 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
useEffect(() => { useEffect(() => {
if (formData.birthDate) { if (formData.birthDate) {
const parts = formData.birthDate.split('-'); const cleanDate = formData.birthDate.substring(0, 10);
const parts = cleanDate.split('-');
if (parts.length === 3) { if (parts.length === 3) {
setBirthDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`); setBirthDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`);
} else { } else {
setBirthDateInput(formData.birthDate); setBirthDateInput(cleanDate);
} }
} else { } else {
setBirthDateInput(''); setBirthDateInput('');
@ -331,11 +335,12 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
useEffect(() => { useEffect(() => {
if (formData.guardianBirthDate) { if (formData.guardianBirthDate) {
const parts = formData.guardianBirthDate.split('-'); const cleanDate = formData.guardianBirthDate.substring(0, 10);
const parts = cleanDate.split('-');
if (parts.length === 3) { if (parts.length === 3) {
setGuardianBirthDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`); setGuardianBirthDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`);
} else { } else {
setGuardianBirthDateInput(formData.guardianBirthDate); setGuardianBirthDateInput(cleanDate);
} }
} else { } else {
setGuardianBirthDateInput(''); setGuardianBirthDateInput('');
@ -344,11 +349,12 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
useEffect(() => { useEffect(() => {
if (formData.rgIssueDate) { if (formData.rgIssueDate) {
const parts = formData.rgIssueDate.split('-'); const cleanDate = formData.rgIssueDate.substring(0, 10);
const parts = cleanDate.split('-');
if (parts.length === 3) { if (parts.length === 3) {
setRgIssueDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`); setRgIssueDateInput(`${parts[2]}/${parts[1]}/${parts[0]}`);
} else { } else {
setRgIssueDateInput(formData.rgIssueDate); setRgIssueDateInput(cleanDate);
} }
} else { } else {
setRgIssueDateInput(''); setRgIssueDateInput('');
@ -839,6 +845,9 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
const studentToSave: Student = { const studentToSave: Student = {
...(editingStudent || { id: studentId }), ...(editingStudent || { id: studentId }),
...formData as Student, ...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, enrollmentNumber,
portalPassword, portalPassword,
photo: finalPhotoUrl photo: finalPhotoUrl
@ -925,7 +934,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
content = content.replace(/{{aluno}}/g, studentToSave.name || ''); content = content.replace(/{{aluno}}/g, studentToSave.name || '');
content = content.replace(/{{aluno_cpf}}/g, studentToSave.cpf || ''); content = content.replace(/{{aluno_cpf}}/g, studentToSave.cpf || '');
content = content.replace(/{{aluno_rg}}/g, studentToSave.rg || ''); 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_email}}/g, studentToSave.email || '');
content = content.replace(/{{aluno_telefone}}/g, studentToSave.phone || ''); content = content.replace(/{{aluno_telefone}}/g, studentToSave.phone || '');
content = content.replace(/{{aluno_cep}}/g, studentToSave.addressZip || ''); content = content.replace(/{{aluno_cep}}/g, studentToSave.addressZip || '');
@ -937,7 +946,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
// Responsável // Responsável
content = content.replace(/{{responsavel_nome}}/g, studentToSave.guardianName || ''); content = content.replace(/{{responsavel_nome}}/g, studentToSave.guardianName || '');
content = content.replace(/{{responsavel_cpf}}/g, studentToSave.guardianCpf || ''); 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 // Curso e Turma
content = content.replace(/{{curso}}/g, course.name || ''); content = content.replace(/{{curso}}/g, course.name || '');
@ -1186,7 +1195,8 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
discount: 0, discount: 0,
hasGuardian: false, hasGuardian: false,
generateFee: true, generateFee: true,
generateContract: true generateContract: true,
sexo: ''
}; };
if (student) { if (student) {
@ -1602,14 +1612,29 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
<User size={14} /> Dados Pessoais <User size={14} /> Dados Pessoais
</h4> </h4>
<div> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Nome Completo do Aluno</label> <div className="md:col-span-2">
<input <label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Nome Completo do Aluno</label>
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-sm" <input
value={formData.name || ''} className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-sm"
onChange={e => setFormData({...formData, name: e.target.value})} value={formData.name || ''}
placeholder="Ex: João da Silva" onChange={e => setFormData({...formData, name: e.target.value})}
/> placeholder="Ex: João da Silva"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Sexo / Gênero</label>
<select
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-sm text-slate-700"
value={formData.sexo || ''}
onChange={e => setFormData({...formData, sexo: e.target.value})}
>
<option value="">Selecione...</option>
<option value="Feminino">Feminino</option>
<option value="Masculino">Masculino</option>
<option value="Outro">Outro</option>
</select>
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

19
manager/fix_all_dates.cjs Normal file
View File

@ -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);

14
manager/fix_dates.cjs Normal file
View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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!");

View File

@ -641,6 +641,20 @@ export async function deleteCategoriaFuncionario(id) {
// ============================================================ // ============================================================
// ALUNOS (FASE 4) // 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() { export async function getAlunos() {
const result = await pool.query("SELECT * FROM alunos ORDER BY nome ASC"); const result = await pool.query("SELECT * FROM alunos ORDER BY nome ASC");
return result.rows.map(r => ({ return result.rows.map(r => ({
@ -649,17 +663,17 @@ export async function getAlunos() {
name: r.nome, name: r.nome,
email: r.email, email: r.email,
phone: r.telefone, phone: r.telefone,
birthDate: r.data_nascimento, birthDate: formatDateOnly(r.data_nascimento),
cpf: r.cpf, cpf: r.cpf,
rg: r.rg, rg: r.rg,
rgIssueDate: r.rg_data_emissao, rgIssueDate: formatDateOnly(r.rg_data_emissao),
guardianName: r.nome_responsavel, guardianName: r.nome_responsavel,
guardianPhone: r.telefone_responsavel, guardianPhone: r.telefone_responsavel,
guardianCpf: r.cpf_responsavel, guardianCpf: r.cpf_responsavel,
guardianBirthDate: r.data_nascimento_responsavel, guardianBirthDate: formatDateOnly(r.data_nascimento_responsavel),
classId: r.turma_id, classId: r.turma_id,
status: r.status, status: r.status,
registrationDate: r.data_matricula, registrationDate: formatDateOnly(r.data_matricula),
photo: r.foto_url, photo: r.foto_url,
addressZip: r.cep, addressZip: r.cep,
addressStreet: r.rua, addressStreet: r.rua,
@ -673,59 +687,72 @@ export async function getAlunos() {
enrollmentNumber: r.numero_matricula, enrollmentNumber: r.numero_matricula,
portalPassword: r.senha_portal, portalPassword: r.senha_portal,
cancellationReason: r.motivo_cancelamento, cancellationReason: r.motivo_cancelamento,
faceDescriptor: r.face_descriptor faceDescriptor: r.face_descriptor,
sexo: r.sexo
})); }));
} }
export async function insertAluno(a) { 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( const result = await pool.query(
`INSERT INTO alunos ( `INSERT INTO alunos (
id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao, id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao,
nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel, nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel,
turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado, turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado,
desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal, desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal,
motivo_cancelamento, face_descriptor motivo_cancelamento, face_descriptor, sexo
) VALUES ( ) 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 *`, ) RETURNING *`,
[ [
a.id, a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null, a.id, a.nome || a.name, a.email || '', a.telefone || a.phone || '', birthDate,
a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null, a.cpf || '', a.rg || '', rgIssueDate,
a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '', a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '',
a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null, a.cpf_responsavel || a.guardianCpf || '', guardianBirthDate,
a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null, 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
]
);
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.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '', 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.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.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.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.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null,
a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : 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 id
] ]
); );
@ -1146,8 +1173,8 @@ export async function syncJsonToRelationalTables() {
id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao, id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao,
nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel, nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel,
turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado, 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 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) ) 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 ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome, email = EXCLUDED.email, telefone = EXCLUDED.telefone, data_nascimento = EXCLUDED.data_nascimento, 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, 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, bairro = EXCLUDED.bairro, cidade = EXCLUDED.cidade, estado = EXCLUDED.estado,
desconto = EXCLUDED.desconto, tem_responsavel = EXCLUDED.tem_responsavel, desconto = EXCLUDED.desconto, tem_responsavel = EXCLUDED.tem_responsavel,
modelo_contrato_id = EXCLUDED.modelo_contrato_id, numero_matricula = EXCLUDED.numero_matricula, 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.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.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.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.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.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
] ]
); );
} }

View File

@ -25,5 +25,10 @@
}, },
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true "noEmit": true
} },
"exclude": [
"node_modules",
"scratch",
"dist"
]
} }

View File

@ -51,6 +51,7 @@ export interface Student {
contractTemplateId?: string; // Vínculo com o modelo de contrato contractTemplateId?: string; // Vínculo com o modelo de contrato
enrollmentNumber?: string; // Número de matrícula (login do portal do aluno) 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) portalPassword?: string; // Senha do portal do aluno (padrão: 6 primeiros dígitos do CPF)
sexo?: string;
} }
export interface Class { export interface Class {

View File

@ -387,7 +387,7 @@ export default function Financeiro() {
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--color-text-secondary)' }}>Valor Pago:</span> <span style={{ color: 'var(--color-text-secondary)' }}>Valor Pago:</span>
<span style={{ fontWeight: 600 }}>{formatCurrency(getEffectiveValue(receiptPayment))}</span> <span style={{ fontWeight: 600 }}>{formatCurrency(getDisplayValue(receiptPayment))}</span>
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--color-text-secondary)' }}>Data de Vencimento:</span> <span style={{ color: 'var(--color-text-secondary)' }}>Data de Vencimento:</span>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAuth } from '../context/AuthContext'; 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 type { Attendance, Lesson } from '../types';
import { getLessonTimeStatus, getNormalizedDate, isLessonWithinJustificationWindow, parseLessonDateTime } from '../lib/lessonUtils'; import { getLessonTimeStatus, getNormalizedDate, isLessonWithinJustificationWindow, parseLessonDateTime } from '../lib/lessonUtils';
import { useRealTimeDate } from '../hooks/useRealTimeDate'; import { useRealTimeDate } from '../hooks/useRealTimeDate';