feat: add student sexo field and update detailed dashboard
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m18s
Details
|
|
@ -0,0 +1,4 @@
|
|||
runner:
|
||||
insecure: false
|
||||
container:
|
||||
network: "network_public"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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')
|
||||
|
|
@ -303,8 +303,12 @@ const Classes: React.FC<ClassesProps> = ({ 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())) {
|
||||
|
|
|
|||
|
|
@ -436,9 +436,9 @@ const Dashboard: React.FC<DashboardProps> = ({ data }) => {
|
|||
<PieChart>
|
||||
<Pie
|
||||
data={[
|
||||
{ name: 'Masculino', value: data.students.filter(s => 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<DashboardProps> = ({ data }) => {
|
|||
dataKey="value"
|
||||
label
|
||||
>
|
||||
<Cell fill="#3b82f6" />
|
||||
<Cell fill="#ec4899" />
|
||||
<Cell fill="#3b82f6" />
|
||||
<Cell fill="#94a3b8" />
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ const Students: React.FC<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ 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<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
|||
discount: 0,
|
||||
hasGuardian: false,
|
||||
generateFee: true,
|
||||
generateContract: true
|
||||
generateContract: true,
|
||||
sexo: ''
|
||||
};
|
||||
|
||||
if (student) {
|
||||
|
|
@ -1602,14 +1612,29 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
|||
<User size={14} /> Dados Pessoais
|
||||
</h4>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Nome Completo do Aluno</label>
|
||||
<input
|
||||
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"
|
||||
value={formData.name || ''}
|
||||
onChange={e => setFormData({...formData, name: e.target.value})}
|
||||
placeholder="Ex: João da Silva"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Nome Completo do Aluno</label>
|
||||
<input
|
||||
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"
|
||||
value={formData.name || ''}
|
||||
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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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!");
|
||||
|
|
@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,10 @@
|
|||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"scratch",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ export default function Financeiro() {
|
|||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<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 style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)' }}>Data de Vencimento:</span>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||