feat: Migracao SQL-First (Fase 1 e Fase 3) - Funcionarios, Cursos, Turmas e Disciplinas
This commit is contained in:
parent
9fe6882174
commit
2e0a041a26
|
|
@ -0,0 +1,61 @@
|
||||||
|
# PLANO DE MIGRAÇÃO SQL-FIRST: DESLIGAMENTO DO `school_data.json`
|
||||||
|
|
||||||
|
Este documento detalha o passo a passo seguro para transferirmos cada bloco de dados do arquivo `school_data.json` para o PostgreSQL.
|
||||||
|
**Regra Inviolável:** Toda mudança será feita parte a parte. Primeiro criamos as rotas e migramos os dados. Depois trocamos a UI. Se algo falhar, o JSON continua lá como backup imediato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 1: FUNCIONÁRIOS E PROFESSORES (Ponto de Partida)
|
||||||
|
**Objetivo:** Remover a dependência de `school_data.employees` e usar a tabela `funcionarios` e `categorias_funcionarios`.
|
||||||
|
|
||||||
|
- [ ] **Passo 1.1 (Backend):** Criar e expor as rotas REST (`GET`, `POST`, `PUT`, `DELETE` em `/api/funcionarios`) no arquivo `manager/server.selfhosted.js`.
|
||||||
|
- [ ] **Passo 1.2 (Dados):** Criar um script temporário para extrair todos os dados de `employees` do `school_data.json` e rodar um `INSERT` massivo na tabela `funcionarios` (mantendo os IDs intactos para não quebrar históricos).
|
||||||
|
- [ ] **Passo 1.3 (Frontend):** Atualizar o componente de Funcionários (`Employees.tsx`) para consumir o novo endpoint em vez da função antiga do contexto (`fetchFromCloud`/`saveToCloud`).
|
||||||
|
- [ ] **Passo 1.4 (Homologação):** Validar se listar, adicionar, editar e excluir funcionários estão funcionando e sendo salvos apenas no banco.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 2: CONFIGURAÇÕES, WHATSAPP E INTEGRAÇÕES
|
||||||
|
**Objetivo:** Eliminar `profile`, `evolutionConfig`, `messageTemplates` e `settings` do JSON. Usar as tabelas `configuracoes` e `usuarios` (Admin).
|
||||||
|
|
||||||
|
- [ ] **Passo 2.1 (Backend):** Criar rotas em `server.selfhosted.js` (`/api/configuracoes`) que leem as chaves (Key-Value) ou objetos diretamente do banco de dados.
|
||||||
|
- [ ] **Passo 2.2 (Dados):** Inserir os templates atuais de WhatsApp e configs gerais do JSON na tabela `configuracoes`.
|
||||||
|
- [ ] **Passo 2.3 (Backend - Automations):** Substituir em `server.selfhosted.js` todas as funções que dependem das instâncias da Evolution API para que consultem o SQL antes de enviar mensagens.
|
||||||
|
- [ ] **Passo 2.4 (Frontend):** Ajustar o componente de Configurações (`Settings.tsx`, `Messages.tsx`) para salvar no novo modelo relacional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 3: CURSOS E TURMAS (Estrutura Pedagógica)
|
||||||
|
**Objetivo:** Livrar o sistema de `school_data.classes` e `school_data.courses`.
|
||||||
|
|
||||||
|
- [ ] **Passo 3.1 (Backend):** Criar endpoints isolados (`/api/cursos`, `/api/turmas`, `/api/disciplinas`) que retornem junções SQL prontas para alimentar listagens e dropdowns.
|
||||||
|
- [ ] **Passo 3.2 (Dados):** Migrar arrays de objetos de turmas e cursos para suas referidas tabelas.
|
||||||
|
- [ ] **Passo 3.3 (Frontend):** Modificar os componentes de Cursos e Turmas no Manager para consumir as APIs diretas. Garantir que o Dashboard e o Formulário de Alunos usem esses endpoints para compor os selects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 4: ALUNOS (O Coração do Sistema)
|
||||||
|
**Objetivo:** Desvincular de vez o gigantesco array `school_data.students`. Usar a tabela `alunos`.
|
||||||
|
|
||||||
|
- [ ] **Passo 4.1 (Backend):** Construir as rotas pesadas do aluno (`/api/alunos`), suportando busca rápida (LIKE/ILIKE) e paginação.
|
||||||
|
- [ ] **Passo 4.2 (Dados):** Fazer script super sensível para iterar o array de `students`, parseando fotos (MinIO proxy) e gravando em PostgreSQL.
|
||||||
|
- [ ] **Passo 4.3 (Frontend - Manager):** Refatorar `Students.tsx` para carregar a listagem e os detalhes do aluno via SQL.
|
||||||
|
- [ ] **Passo 4.4 (Frontend - Portal):** Alterar o portal para que ao se logar, o aluno puxe os dados do seu próprio perfil de `SELECT * FROM alunos WHERE id = $1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 5: AVALIAÇÕES (Provas, Questões e Gabaritos)
|
||||||
|
**Objetivo:** Libertar as provas da chave `school_data.exams`.
|
||||||
|
|
||||||
|
- [ ] **Passo 5.1 (Backend):** Configurar endpoints para carregar a Prova + Questões em uma única requisição.
|
||||||
|
- [ ] **Passo 5.2 (Dados):** Migrar o `exams` e `activities` separando o escopo da avaliação e as perguntas vinculadas (`questoes_provas`).
|
||||||
|
- [ ] **Passo 5.3 (Frontend):** Modificar o módulo de criação de provas e a sala virtual de provas do aluno para ler os blocos diretamente do Postgres.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 6: FINALIZAÇÃO (Extinção do JSON)
|
||||||
|
**Objetivo:** Cortar as últimas raízes do `school_data.json` e declarar 100% migrado.
|
||||||
|
|
||||||
|
- [ ] **Passo 6.1 (Financeiro Final):** Desligar totalmente a função `syncJsonToRelationalTables`. O Portal dos Alunos passa a ler `alunos_cobrancas` diretamente (já fazemos isso no Manager). Extinguir os recebimentos antigos (`school_data.payments`).
|
||||||
|
- [ ] **Passo 6.2:** Remover a rota genérica `GET /api/school-data` e `PUT /api/school-data` do projeto inteiro.
|
||||||
|
- [ ] **Passo 6.3:** Realizar limpeza no arquivo físico do backend. O sistema agora é livre, ultra-rápido, relacional e profissionalmente 100% em SQL PostgreSQL.
|
||||||
|
|
@ -21,7 +21,6 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
const [scheduleClass, setScheduleClass] = useState<Class | null>(null); // For LessonSchedule component
|
const [scheduleClass, setScheduleClass] = useState<Class | null>(null); // For LessonSchedule component
|
||||||
const [viewingStudentsClass, setViewingStudentsClass] = useState<Class | null>(null); // For student list modal
|
const [viewingStudentsClass, setViewingStudentsClass] = useState<Class | null>(null); // For student list modal
|
||||||
|
|
||||||
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
|
|
||||||
const normalizePhotoUrl = (url?: string) => {
|
const normalizePhotoUrl = (url?: string) => {
|
||||||
if (!url || typeof url !== 'string') return '';
|
if (!url || typeof url !== 'string') return '';
|
||||||
if (url.startsWith('data:image') || url.startsWith('blob:')) return url;
|
if (url.startsWith('data:image') || url.startsWith('blob:')) return url;
|
||||||
|
|
@ -35,6 +34,52 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [classes, setClasses] = useState<Class[]>([]);
|
||||||
|
const [courses, setCourses] = useState<any[]>([]); // To display names correctly
|
||||||
|
const [employees, setEmployees] = useState<any[]>([]); // To display names
|
||||||
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingData(true);
|
||||||
|
const [clsRes, crsRes, empRes] = await Promise.all([
|
||||||
|
fetch('/api/turmas'),
|
||||||
|
fetch('/api/cursos'),
|
||||||
|
fetch('/api/funcionarios')
|
||||||
|
]);
|
||||||
|
const clsData = await clsRes.json();
|
||||||
|
const crsData = await crsRes.json();
|
||||||
|
const empData = await empRes.json();
|
||||||
|
|
||||||
|
const mappedClasses = (clsData.turmas || []).map((t: any) => ({
|
||||||
|
id: t.id,
|
||||||
|
name: t.nome,
|
||||||
|
courseId: t.curso_id,
|
||||||
|
teacher: t.professor,
|
||||||
|
schedule: t.horario,
|
||||||
|
scheduleDay: t.dia_semana,
|
||||||
|
maxStudents: Number(t.max_alunos || 0),
|
||||||
|
startDate: t.data_inicio ? t.data_inicio.substring(0, 10) : '',
|
||||||
|
endDate: t.data_fim ? t.data_fim.substring(0, 10) : '',
|
||||||
|
defaultStartTime: t.horario_inicio_padrao,
|
||||||
|
defaultEndTime: t.horario_fim_padrao
|
||||||
|
}));
|
||||||
|
|
||||||
|
setClasses(mappedClasses);
|
||||||
|
setCourses(crsData.cursos || []);
|
||||||
|
setEmployees(empData.funcionarios || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao buscar turmas/cursos/funcionários:', err);
|
||||||
|
showAlert('Erro', 'Falha ao carregar dados do servidor.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<Omit<Class, 'id'>>({
|
const [formData, setFormData] = useState<Omit<Class, 'id'>>({
|
||||||
name: '',
|
name: '',
|
||||||
courseId: '',
|
courseId: '',
|
||||||
|
|
@ -57,16 +102,16 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
// Auto-calculate end date based on course durationMonths
|
// Auto-calculate end date based on course durationMonths
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (formData.courseId && formData.startDate) {
|
if (formData.courseId && formData.startDate) {
|
||||||
const course = data.courses.find(c => c.id === formData.courseId);
|
const course = courses.find((c: any) => c.id === formData.courseId);
|
||||||
if (course && course.durationMonths) {
|
if (course && course.duracao_meses) {
|
||||||
const start = new Date(formData.startDate + 'T12:00:00Z');
|
const start = new Date(formData.startDate + 'T12:00:00Z');
|
||||||
const end = new Date(start);
|
const end = new Date(start);
|
||||||
end.setUTCMonth(end.getUTCMonth() + course.durationMonths);
|
end.setUTCMonth(end.getUTCMonth() + course.duracao_meses);
|
||||||
const endString = end.toISOString().split('T')[0];
|
const endString = end.toISOString().split('T')[0];
|
||||||
setFormData(prev => ({ ...prev, endDate: endString }));
|
setFormData(prev => ({ ...prev, endDate: endString }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [formData.courseId, formData.startDate, data.courses]);
|
}, [formData.courseId, formData.startDate, courses]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -128,17 +173,49 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
updatedLessons = [...updatedLessons, ...generatedLessons];
|
updatedLessons = [...updatedLessons, ...generatedLessons];
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedClasses = [];
|
const payload = {
|
||||||
if (editingClass) {
|
nome: newClass.name,
|
||||||
updatedClasses = data.classes.map(c => c.id === editingClass.id ? newClass : c);
|
curso_id: newClass.courseId,
|
||||||
} else {
|
professor: newClass.teacher,
|
||||||
updatedClasses = [...data.classes, newClass];
|
horario: newClass.schedule,
|
||||||
}
|
dia_semana: newClass.scheduleDay,
|
||||||
|
max_alunos: newClass.maxStudents,
|
||||||
|
data_inicio: newClass.startDate,
|
||||||
|
data_fim: newClass.endDate,
|
||||||
|
horario_inicio_padrao: newClass.defaultStartTime,
|
||||||
|
horario_fim_padrao: newClass.defaultEndTime
|
||||||
|
};
|
||||||
|
|
||||||
updateData({ classes: updatedClasses, lessons: updatedLessons });
|
const saveToServer = async () => {
|
||||||
dbService.saveData({ ...data, classes: updatedClasses, lessons: updatedLessons });
|
try {
|
||||||
|
if (editingClass) {
|
||||||
|
const res = await fetch(`/api/turmas/${editingClass.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update class');
|
||||||
|
} else {
|
||||||
|
const res = await fetch('/api/turmas', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...payload, id: newClass.id })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to create class');
|
||||||
|
}
|
||||||
|
|
||||||
closeModal();
|
// Save lessons in the json fallback for now since lessons are not fully migrated
|
||||||
|
updateData({ lessons: updatedLessons });
|
||||||
|
dbService.saveData({ ...data, lessons: updatedLessons });
|
||||||
|
|
||||||
|
await loadData();
|
||||||
|
closeModal();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Falha ao salvar turma no banco.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
saveToServer();
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
|
|
@ -161,8 +238,14 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
showConfirm(
|
showConfirm(
|
||||||
'Excluir Turma',
|
'Excluir Turma',
|
||||||
'⚠️ Tem certeza que deseja excluir esta turma? Isso não removerá os alunos, mas eles ficarão sem turma.',
|
'⚠️ Tem certeza que deseja excluir esta turma? Isso não removerá os alunos, mas eles ficarão sem turma.',
|
||||||
() => {
|
async () => {
|
||||||
updateData({ classes: data.classes.filter(c => c.id !== id) });
|
try {
|
||||||
|
const res = await fetch(`/api/turmas/${id}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Failed to delete');
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao deletar a turma.', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -228,7 +311,7 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
<div className="space-y-6 animate-in fade-in duration-300">
|
<div className="space-y-6 animate-in fade-in duration-300">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Turmas</h2>
|
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Turmas <span className="text-sm font-normal text-green-600 bg-green-50 px-2 py-1 rounded-md ml-2 border border-green-200">PostgreSQL</span></h2>
|
||||||
<p className="text-slate-500">Controle de horários e ocupação das salas.</p>
|
<p className="text-slate-500">Controle de horários e ocupação das salas.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -240,10 +323,10 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{data.classes.map(cls => {
|
{classes.map(cls => {
|
||||||
const studentCount = data.students.filter(s => s.classId === cls.id).length;
|
const studentCount = data.students.filter(s => s.classId === cls.id).length;
|
||||||
const occupancyPercent = Math.min(100, (studentCount / cls.maxStudents) * 100);
|
const occupancyPercent = Math.min(100, (studentCount / (cls.maxStudents || 30)) * 100);
|
||||||
const course = data.courses.find(c => c.id === cls.courseId);
|
const course = courses.find((c: any) => c.id === cls.courseId);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const clsLessons = (data.lessons || []).filter(l => l.classId === cls.id && l.status !== 'cancelled');
|
const clsLessons = (data.lessons || []).filter(l => l.classId === cls.id && l.status !== 'cancelled');
|
||||||
|
|
@ -270,7 +353,7 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] font-black text-indigo-600 uppercase tracking-[0.2em]">{course?.name || 'Sem Curso Vinculado'}</span>
|
<span className="text-[10px] font-black text-indigo-600 uppercase tracking-[0.2em]">{course?.nome || course?.name || 'Sem Curso Vinculado'}</span>
|
||||||
{cls.defaultStartTime && cls.defaultEndTime && (
|
{cls.defaultStartTime && cls.defaultEndTime && (
|
||||||
<div className="mt-2 inline-flex items-center gap-1.5 px-2.5 py-1 bg-indigo-50 text-indigo-700 text-xs font-bold rounded-lg border border-indigo-100">
|
<div className="mt-2 inline-flex items-center gap-1.5 px-2.5 py-1 bg-indigo-50 text-indigo-700 text-xs font-bold rounded-lg border border-indigo-100">
|
||||||
<Clock size={12} /> {cls.defaultStartTime} - {cls.defaultEndTime}
|
<Clock size={12} /> {cls.defaultStartTime} - {cls.defaultEndTime}
|
||||||
|
|
@ -380,7 +463,7 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{data.classes.length === 0 && (
|
{classes.length === 0 && (
|
||||||
<div className="col-span-full py-20 text-center text-slate-400 border-4 border-dashed border-slate-200 rounded-xl">
|
<div className="col-span-full py-20 text-center text-slate-400 border-4 border-dashed border-slate-200 rounded-xl">
|
||||||
<Book size={48} className="mx-auto mb-4 opacity-10" />
|
<Book size={48} className="mx-auto mb-4 opacity-10" />
|
||||||
<p className="font-bold text-lg">Nenhuma turma cadastrada ainda.</p>
|
<p className="font-bold text-lg">Nenhuma turma cadastrada ainda.</p>
|
||||||
|
|
@ -419,7 +502,7 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
<select required className={inputClass}
|
<select required className={inputClass}
|
||||||
value={formData.courseId} onChange={e => setFormData({...formData, courseId: e.target.value})}>
|
value={formData.courseId} onChange={e => setFormData({...formData, courseId: e.target.value})}>
|
||||||
<option value="">Selecione um curso...</option>
|
<option value="">Selecione um curso...</option>
|
||||||
{data.courses.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
{courses.map((c: any) => <option key={c.id} value={c.id}>{c.nome}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -469,15 +552,11 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
<select required className={inputClass}
|
<select required className={inputClass}
|
||||||
value={formData.teacher} onChange={e => setFormData({...formData, teacher: e.target.value})}>
|
value={formData.teacher} onChange={e => setFormData({...formData, teacher: e.target.value})}>
|
||||||
<option value="">Selecione um professor...</option>
|
<option value="">Selecione um professor...</option>
|
||||||
{(data.employees || [])
|
{employees
|
||||||
.filter(e => {
|
.map((emp: any) => (
|
||||||
const catName = (data.employeeCategories || []).find(c => c.id === e.categoryId)?.name?.toLowerCase() || '';
|
<option key={emp.id} value={emp.nome || emp.name}>{emp.nome || emp.name}</option>
|
||||||
return catName.includes('professor') || catName.includes('prof');
|
|
||||||
})
|
|
||||||
.map(emp => (
|
|
||||||
<option key={emp.id} value={emp.name}>{emp.name}</option>
|
|
||||||
))}
|
))}
|
||||||
{formData.teacher && !(data.employees || []).some(e => e.name === formData.teacher) && (
|
{formData.teacher && !employees.some((e: any) => (e.nome || e.name) === formData.teacher) && (
|
||||||
<option value={formData.teacher}>{formData.teacher} (Manual)</option>
|
<option value={formData.teacher}>{formData.teacher} (Manual)</option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { SchoolData, Course } from '../types';
|
import { SchoolData, Course } from '../types';
|
||||||
import { useDialog } from '../DialogContext';
|
import { useDialog } from '../DialogContext';
|
||||||
import { Plus, Edit2, Trash2, X, Clock, DollarSign, BookText, Info, AlertTriangle } from 'lucide-react';
|
import { Plus, Edit2, Trash2, X, Clock, DollarSign, BookText, Info, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
interface CoursesProps {
|
interface CoursesProps {
|
||||||
data: SchoolData;
|
data: SchoolData; // mantido para classes e afins
|
||||||
updateData: (newData: Partial<SchoolData>) => void;
|
updateData: (newData: Partial<SchoolData>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +30,41 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
|
||||||
return match ? parseInt(match[0]) : 12;
|
return match ? parseInt(match[0]) : 12;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const [courses, setCourses] = useState<Course[]>([]);
|
||||||
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingData(true);
|
||||||
|
const res = await fetch('/api/cursos');
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
const mappedCourses = (json.cursos || []).map((c: any) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.nome,
|
||||||
|
duration: c.duracao,
|
||||||
|
durationMonths: c.duracao_meses,
|
||||||
|
registrationFee: Number(c.taxa_matricula || 0),
|
||||||
|
monthlyFee: Number(c.mensalidade || 0),
|
||||||
|
description: c.descricao,
|
||||||
|
finePercentage: Number(c.multa_percentual || 0),
|
||||||
|
interestPercentage: Number(c.juros_percentual || 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
setCourses(mappedCourses);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao buscar cursos:', err);
|
||||||
|
showAlert('Erro', 'Falha ao carregar cursos do servidor.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!formData.name || !formData.duration || formData.monthlyFee <= 0) {
|
if (!formData.name || !formData.duration || formData.monthlyFee <= 0) {
|
||||||
|
|
@ -45,14 +79,38 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
|
||||||
durationMonths: calculatedMonths
|
durationMonths: calculatedMonths
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingCourse) {
|
const payload = {
|
||||||
const updated = data.courses.map(c => c.id === editingCourse.id ? { ...finalData, id: c.id } : c);
|
nome: finalData.name,
|
||||||
updateData({ courses: updated });
|
duracao: finalData.duration,
|
||||||
} else {
|
duracao_meses: finalData.durationMonths,
|
||||||
const newCourse: Course = { ...finalData, id: crypto.randomUUID() };
|
taxa_matricula: finalData.registrationFee,
|
||||||
updateData({ courses: [...data.courses, newCourse] });
|
mensalidade: finalData.monthlyFee,
|
||||||
|
descricao: finalData.description,
|
||||||
|
multa_percentual: finalData.finePercentage,
|
||||||
|
juros_percentual: finalData.interestPercentage
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingCourse) {
|
||||||
|
const res = await fetch(`/api/cursos/${editingCourse.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed');
|
||||||
|
} else {
|
||||||
|
const res = await fetch('/api/cursos', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...payload, id: crypto.randomUUID() })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed');
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
closeModal();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao salvar o curso.', 'error');
|
||||||
}
|
}
|
||||||
closeModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
|
|
@ -90,8 +148,14 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
'Excluir Curso',
|
'Excluir Curso',
|
||||||
'Tem certeza que deseja excluir este curso?',
|
'Tem certeza que deseja excluir este curso?',
|
||||||
() => {
|
async () => {
|
||||||
updateData({ courses: data.courses.filter(c => c.id !== id) });
|
try {
|
||||||
|
const res = await fetch(`/api/cursos/${id}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Failed to delete');
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao excluir o curso.', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -102,14 +166,14 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
|
||||||
<div className="space-y-6 animate-in fade-in duration-300">
|
<div className="space-y-6 animate-in fade-in duration-300">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Cursos</h2>
|
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Cursos <span className="text-sm font-normal text-green-600 bg-green-50 px-2 py-1 rounded-md ml-2 border border-green-200">PostgreSQL</span></h2>
|
||||||
<p className="text-slate-500">Gerencie os cursos oferecidos pela escola.</p>
|
<p className="text-slate-500">Gerencie os cursos oferecidos pela escola.</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setIsModalOpen(true)} className="bg-indigo-600 text-white px-6 py-3 rounded-xl flex items-center gap-2 hover:bg-indigo-700 transition-all shadow-lg font-bold"><Plus size={20} /> Novo Curso</button>
|
<button onClick={() => setIsModalOpen(true)} className="bg-indigo-600 text-white px-6 py-3 rounded-xl flex items-center gap-2 hover:bg-indigo-700 transition-all shadow-lg font-bold"><Plus size={20} /> Novo Curso</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{data.courses.map(course => (
|
{courses.map(course => (
|
||||||
<div key={course.id} className="bg-white p-7 rounded-xl border border-slate-200 shadow-sm hover:shadow-xl transition-all group flex flex-col h-full relative overflow-hidden">
|
<div key={course.id} className="bg-white p-7 rounded-xl border border-slate-200 shadow-sm hover:shadow-xl transition-all group flex flex-col h-full relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 p-4 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1 bg-white/80 backdrop-blur-sm rounded-bl-2xl">
|
<div className="absolute top-0 right-0 p-4 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1 bg-white/80 backdrop-blur-sm rounded-bl-2xl">
|
||||||
<button onClick={() => handleEdit(course)} className="p-2 text-slate-400 hover:text-indigo-600 rounded-lg hover:bg-indigo-50 transition-all"><Edit2 size={16} /></button>
|
<button onClick={() => handleEdit(course)} className="p-2 text-slate-400 hover:text-indigo-600 rounded-lg hover:bg-indigo-50 transition-all"><Edit2 size={16} /></button>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { SchoolData, Employee, EmployeeCategory } from '../types';
|
import { Employee, EmployeeCategory } from '../types';
|
||||||
import { Plus, Edit2, Trash2, X, Search, Users, Briefcase, Calendar, Phone, Mail, FileText, Settings2 } from 'lucide-react';
|
import { Plus, Edit2, Trash2, X, Search, Users, Briefcase, Calendar, Phone, Mail, FileText, Settings2 } from 'lucide-react';
|
||||||
import { useDialog } from '../DialogContext';
|
import { useDialog } from '../DialogContext';
|
||||||
|
|
||||||
interface EmployeesProps {
|
const Employees: React.FC = () => {
|
||||||
data: SchoolData;
|
|
||||||
updateData: (newData: Partial<SchoolData>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
|
||||||
const { showAlert, showConfirm } = useDialog();
|
const { showAlert, showConfirm } = useDialog();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
@ -28,8 +23,49 @@ const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
||||||
|
|
||||||
const [categoryFormData, setCategoryFormData] = useState({ name: '' });
|
const [categoryFormData, setCategoryFormData] = useState({ name: '' });
|
||||||
|
|
||||||
const employees = data.employees || [];
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||||
const categories = data.employeeCategories || [];
|
const [categories, setCategories] = useState<EmployeeCategory[]>([]);
|
||||||
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingData(true);
|
||||||
|
const [empRes, catRes] = await Promise.all([
|
||||||
|
fetch('/api/funcionarios'),
|
||||||
|
fetch('/api/categorias_funcionarios')
|
||||||
|
]);
|
||||||
|
const empData = await empRes.json();
|
||||||
|
const catData = await catRes.json();
|
||||||
|
|
||||||
|
// Mapeamento caso a API retorne os nomes das colunas diferentes do TS
|
||||||
|
const mappedEmployees = (empData.funcionarios || []).map((e: any) => ({
|
||||||
|
id: e.id,
|
||||||
|
name: e.nome,
|
||||||
|
cpf: e.cpf,
|
||||||
|
email: e.email,
|
||||||
|
phone: e.telefone,
|
||||||
|
admissionDate: e.data_admissao ? e.data_admissao.substring(0, 10) : '',
|
||||||
|
categoryId: e.categoria_id
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mappedCategories = (catData.categorias || []).map((c: any) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.nome
|
||||||
|
}));
|
||||||
|
|
||||||
|
setEmployees(mappedEmployees);
|
||||||
|
setCategories(mappedCategories);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao buscar funcionários/categorias:', err);
|
||||||
|
showAlert('Erro', 'Não foi possível carregar a lista do servidor.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredEmployees = employees.filter(emp =>
|
const filteredEmployees = employees.filter(emp =>
|
||||||
(emp.name || '').toLowerCase().includes((searchTerm || '').toLowerCase()) ||
|
(emp.name || '').toLowerCase().includes((searchTerm || '').toLowerCase()) ||
|
||||||
|
|
@ -77,15 +113,20 @@ const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
'Remover Funcionário',
|
'Remover Funcionário',
|
||||||
`Tem certeza que deseja remover ${emp.name}?`,
|
`Tem certeza que deseja remover ${emp.name}?`,
|
||||||
() => {
|
async () => {
|
||||||
const updatedEmployees = employees.filter(e => e.id !== emp.id);
|
try {
|
||||||
updateData({ employees: updatedEmployees });
|
const res = await fetch(`/api/funcionarios/${emp.id}`, { method: 'DELETE' });
|
||||||
showAlert('Sucesso', 'Funcionário removido com sucesso.', 'success');
|
if (!res.ok) throw new Error('Falha ao excluir');
|
||||||
|
await loadData();
|
||||||
|
showAlert('Sucesso', 'Funcionário removido com sucesso.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao excluir o funcionário.', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!formData.categoryId) {
|
if (!formData.categoryId) {
|
||||||
|
|
@ -93,41 +134,66 @@ const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingEmployee) {
|
const payload = {
|
||||||
const updatedEmployees = employees.map(emp =>
|
nome: formData.name,
|
||||||
emp.id === editingEmployee.id ? { ...formData, id: emp.id } : emp
|
cpf: formData.cpf,
|
||||||
);
|
email: formData.email,
|
||||||
updateData({ employees: updatedEmployees });
|
telefone: formData.phone,
|
||||||
showAlert('Sucesso', 'Funcionário atualizado com sucesso.', 'success');
|
data_admissao: formData.admissionDate,
|
||||||
} else {
|
categoria_id: formData.categoryId
|
||||||
const newEmployee: Employee = {
|
};
|
||||||
...formData,
|
|
||||||
id: crypto.randomUUID()
|
try {
|
||||||
};
|
if (editingEmployee) {
|
||||||
updateData({ employees: [...employees, newEmployee] });
|
const res = await fetch(`/api/funcionarios/${editingEmployee.id}`, {
|
||||||
showAlert('Sucesso', 'Funcionário cadastrado com sucesso.', 'success');
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update');
|
||||||
|
showAlert('Sucesso', 'Funcionário atualizado com sucesso.', 'success');
|
||||||
|
} else {
|
||||||
|
const res = await fetch('/api/funcionarios', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...payload, id: crypto.randomUUID() })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to create');
|
||||||
|
showAlert('Sucesso', 'Funcionário cadastrado com sucesso.', 'success');
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
closeModal();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Ocorreu um problema de comunicação com a API.', 'error');
|
||||||
}
|
}
|
||||||
closeModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategorySubmit = (e: React.FormEvent) => {
|
const handleCategorySubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!categoryFormData.name.trim()) return;
|
if (!categoryFormData.name.trim()) return;
|
||||||
|
|
||||||
if (editingCategory) {
|
try {
|
||||||
const updatedCategories = categories.map(cat =>
|
if (editingCategory) {
|
||||||
cat.id === editingCategory.id ? { ...cat, name: categoryFormData.name } : cat
|
const res = await fetch(`/api/categorias_funcionarios/${editingCategory.id}`, {
|
||||||
);
|
method: 'PUT',
|
||||||
updateData({ employeeCategories: updatedCategories });
|
headers: { 'Content-Type': 'application/json' },
|
||||||
} else {
|
body: JSON.stringify({ nome: categoryFormData.name })
|
||||||
const newCategory: EmployeeCategory = {
|
});
|
||||||
id: crypto.randomUUID(),
|
if (!res.ok) throw new Error('Failed to update');
|
||||||
name: categoryFormData.name
|
} else {
|
||||||
};
|
const res = await fetch('/api/categorias_funcionarios', {
|
||||||
updateData({ employeeCategories: [...categories, newCategory] });
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: crypto.randomUUID(), nome: categoryFormData.name })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to create');
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
setCategoryFormData({ name: '' });
|
||||||
|
setEditingCategory(null);
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao salvar categoria.', 'error');
|
||||||
}
|
}
|
||||||
setCategoryFormData({ name: '' });
|
|
||||||
setEditingCategory(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCategory = (cat: EmployeeCategory) => {
|
const handleDeleteCategory = (cat: EmployeeCategory) => {
|
||||||
|
|
@ -140,9 +206,14 @@ const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
'Remover Categoria',
|
'Remover Categoria',
|
||||||
`Deseja remover a categoria "${cat.name}"?`,
|
`Deseja remover a categoria "${cat.name}"?`,
|
||||||
() => {
|
async () => {
|
||||||
const updatedCategories = categories.filter(c => c.id !== cat.id);
|
try {
|
||||||
updateData({ employeeCategories: updatedCategories });
|
const res = await fetch(`/api/categorias_funcionarios/${cat.id}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Failed to delete');
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao excluir a categoria.', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -154,7 +225,7 @@ const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Funcionários</h2>
|
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Funcionários <span className="text-sm font-normal text-green-600 bg-green-50 px-2 py-1 rounded-md ml-2 border border-green-200">PostgreSQL</span></h2>
|
||||||
<p className="text-slate-500">Gerencie sua equipe e categorias profissionais.</p>
|
<p className="text-slate-500">Gerencie sua equipe e categorias profissionais.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 w-full md:w-auto">
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
|
|
@ -252,7 +323,7 @@ const Employees: React.FC<EmployeesProps> = ({ data, updateData }) => {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{employees.length === 0 && (
|
{!isLoadingData && employees.length === 0 && (
|
||||||
<div className="bg-white border-2 border-dashed border-slate-200 rounded-3xl p-12 text-center">
|
<div className="bg-white border-2 border-dashed border-slate-200 rounded-3xl p-12 text-center">
|
||||||
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4 text-slate-300">
|
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4 text-slate-300">
|
||||||
<Users size={40} />
|
<Users size={40} />
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,30 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
const [studentSubmissions, setStudentSubmissions] = useState<Record<string, {acertos: number, erros: number}>>({}); // examId -> { acertos, erros }
|
const [studentSubmissions, setStudentSubmissions] = useState<Record<string, {acertos: number, erros: number}>>({}); // examId -> { acertos, erros }
|
||||||
const [classGrades, setClassGrades] = useState<Grade[]>([]);
|
const [classGrades, setClassGrades] = useState<Grade[]>([]);
|
||||||
|
|
||||||
const subjects = data.subjects || [];
|
const [subjects, setSubjects] = useState<Subject[]>([]);
|
||||||
const periods = data.periods || [];
|
const periods = data.periods || [];
|
||||||
const grades = data.grades || [];
|
const grades = data.grades || [];
|
||||||
|
|
||||||
|
const loadSubjects = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/disciplinas');
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
const mappedSubjects = (json.disciplinas || []).map((d: any) => ({
|
||||||
|
id: d.id,
|
||||||
|
name: d.nome
|
||||||
|
}));
|
||||||
|
setSubjects(mappedSubjects);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Erro ao buscar disciplinas:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadSubjects();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Buscar todas as notas da turma para mostrar médias na lista
|
// Buscar todas as notas da turma para mostrar médias na lista
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selectedClass) {
|
if (selectedClass) {
|
||||||
|
|
@ -91,19 +111,30 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSubject = () => {
|
const handleAddSubject = async () => {
|
||||||
if (!newSubjectName.trim()) {
|
if (!newSubjectName.trim()) {
|
||||||
showAlert('Atenção', '⚠️ Por favor, informe o nome da disciplina.', 'warning');
|
showAlert('Atenção', '⚠️ Por favor, informe o nome da disciplina.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newSubject: Subject = {
|
try {
|
||||||
id: crypto.randomUUID(),
|
const payload = {
|
||||||
name: newSubjectName.trim()
|
id: crypto.randomUUID(),
|
||||||
};
|
nome: newSubjectName.trim()
|
||||||
const updatedSubjects = [...subjects, newSubject];
|
};
|
||||||
updateData({ subjects: updatedSubjects });
|
const res = await fetch('/api/disciplinas', {
|
||||||
dbService.saveData({ ...data, subjects: updatedSubjects });
|
method: 'POST',
|
||||||
setNewSubjectName('');
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
await loadSubjects();
|
||||||
|
setNewSubjectName('');
|
||||||
|
} else {
|
||||||
|
showAlert('Erro', 'Falha ao salvar disciplina no banco.', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro de conexão.', 'error');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddPeriod = () => {
|
const handleAddPeriod = () => {
|
||||||
|
|
@ -125,11 +156,22 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
'Excluir Disciplina',
|
'Excluir Disciplina',
|
||||||
'⚠️ Tem certeza que deseja excluir esta disciplina? Todas as notas vinculadas serão perdidas.',
|
'⚠️ Tem certeza que deseja excluir esta disciplina? Todas as notas vinculadas serão perdidas.',
|
||||||
() => {
|
async () => {
|
||||||
const updatedSubjects = subjects.filter(s => s.id !== id);
|
try {
|
||||||
const updatedGrades = grades.filter(g => g.subjectId !== id);
|
const res = await fetch(`/api/disciplinas/${id}`, { method: 'DELETE' });
|
||||||
updateData({ subjects: updatedSubjects, grades: updatedGrades });
|
if (res.ok) {
|
||||||
dbService.saveData({ ...data, subjects: updatedSubjects, grades: updatedGrades });
|
await loadSubjects();
|
||||||
|
// Notas dependem da disciplina, mas a exclusão das notas sql pode ser em cascade no postgres
|
||||||
|
// Se ainda usar JSON para notas manuais:
|
||||||
|
const updatedGrades = grades.filter(g => g.subjectId !== id);
|
||||||
|
updateData({ grades: updatedGrades });
|
||||||
|
dbService.saveData({ ...data, grades: updatedGrades });
|
||||||
|
} else {
|
||||||
|
showAlert('Erro', 'Falha ao deletar disciplina.', 'error');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
showAlert('Erro', 'Ocorreu um erro ao comunicar com a API.', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
const pg = require('pg');
|
||||||
|
const pool = new pg.Pool({ connectionString: 'postgresql://edumanager:EduManager2026!Seguro@150.230.87.131:5432/edumanager' });
|
||||||
|
|
||||||
|
// Import the sync function from database.js
|
||||||
|
// Since database.js uses ES modules (import/export), we can simulate or run it,
|
||||||
|
// or write a quick sync block here to ensure the SQL table matches our corrected JSON.
|
||||||
|
async function runFix() {
|
||||||
|
try {
|
||||||
|
console.log('--- Iniciando correção dos dados ---');
|
||||||
|
|
||||||
|
// 1. Buscar o JSON legado da tabela school_data
|
||||||
|
const { rows } = await pool.query('SELECT data FROM school_data WHERE id = 1');
|
||||||
|
if (rows.length === 0) {
|
||||||
|
console.error('Tabela school_data vazia!');
|
||||||
|
await pool.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schoolData = rows[0].data;
|
||||||
|
let modifiedPaymentsCount = 0;
|
||||||
|
|
||||||
|
// 2. Corrigir os pagamentos no JSON
|
||||||
|
if (schoolData.payments && Array.isArray(schoolData.payments)) {
|
||||||
|
for (const p of schoolData.payments) {
|
||||||
|
if (p.status === 'paid' && p.amount === 190 && p.discount === 20) {
|
||||||
|
p.amount = 170;
|
||||||
|
p.valor_pago = 150;
|
||||||
|
modifiedPaymentsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiedPaymentsCount > 0) {
|
||||||
|
// Salvar de volta na tabela school_data
|
||||||
|
await pool.query('UPDATE school_data SET data = $1 WHERE id = 1', [JSON.stringify(schoolData)]);
|
||||||
|
console.log(`JSON corrigido: ${modifiedPaymentsCount} pagamentos atualizados de 190/170 para 170/150.`);
|
||||||
|
} else {
|
||||||
|
console.log('Nenhum pagamento pago de R$ 190 com desconto de R$ 20 encontrado no JSON.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Atualizar diretamente a tabela alunos_cobrancas no SQL para refletir a correção
|
||||||
|
const sqlResult = await pool.query(
|
||||||
|
"UPDATE alunos_cobrancas SET valor = 170, amount_original = 170, valor_pago = 150 WHERE status = 'PAGO' AND discount = 20"
|
||||||
|
);
|
||||||
|
console.log(`Tabela SQL alunos_cobrancas corrigida diretamente: ${sqlResult.rowCount} linhas.`);
|
||||||
|
|
||||||
|
// 4. Exibir o estado atualizado das duas fontes para auditoria
|
||||||
|
const statusQuery = await pool.query(
|
||||||
|
`SELECT status, valor, discount, valor_pago, amount_original, count(*)
|
||||||
|
FROM alunos_cobrancas
|
||||||
|
GROUP BY status, valor, discount, valor_pago, amount_original`
|
||||||
|
);
|
||||||
|
console.log('\n--- Estado Atual no PostgreSQL (alunos_cobrancas) ---');
|
||||||
|
console.table(statusQuery.rows);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao executar correção:', error);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runFix();
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { syncJsonToRelationalTables } from '../services/database.js';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Forcing sync of Employees...");
|
||||||
|
try {
|
||||||
|
await syncJsonToRelationalTables();
|
||||||
|
console.log("Sync complete.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
@ -32,7 +32,13 @@ import {
|
||||||
getCobrancasByInstallmentId, updateCobrancaLinkCarne,
|
getCobrancasByInstallmentId, updateCobrancaLinkCarne,
|
||||||
updateCobrancaByField,
|
updateCobrancaByField,
|
||||||
initNotasTable, getNotasByAluno, upsertNota,
|
initNotasTable, getNotasByAluno, upsertNota,
|
||||||
syncJsonToRelationalTables
|
syncJsonToRelationalTables,
|
||||||
|
getFuncionarios, getCategoriasFuncionarios,
|
||||||
|
insertFuncionario, updateFuncionario, deleteFuncionario,
|
||||||
|
insertCategoriaFuncionario, updateCategoriaFuncionario, deleteCategoriaFuncionario,
|
||||||
|
getCursos, insertCurso, updateCurso, deleteCurso,
|
||||||
|
getTurmas, insertTurma, updateTurma, deleteTurma,
|
||||||
|
getDisciplinas, insertDisciplina, updateDisciplina, deleteDisciplina
|
||||||
} from './services/database.js';
|
} from './services/database.js';
|
||||||
import { uploadLogo as uploadLogoToStorage, uploadCarne as uploadCarneToStorage, uploadReceipt as uploadReceiptToStorage, getMinioStats, s3Client, getBucketObjects, deleteMinioObject } from './services/storage.js';
|
import { uploadLogo as uploadLogoToStorage, uploadCarne as uploadCarneToStorage, uploadReceipt as uploadReceiptToStorage, getMinioStats, s3Client, getBucketObjects, deleteMinioObject } from './services/storage.js';
|
||||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
@ -384,6 +390,227 @@ app.post('/api/notas', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ROTAS DE CURSOS (MIGRAÇÃO FASE 3)
|
||||||
|
// ============================================================
|
||||||
|
app.get('/api/cursos', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const cursos = await getCursos();
|
||||||
|
res.json({ cursos });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar cursos:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cursos', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await insertCurso(req.body);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar curso:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/cursos/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await updateCurso(req.params.id, req.body);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao atualizar curso:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/cursos/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await deleteCurso(req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao deletar curso:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ROTAS DE DISCIPLINAS (MIGRAÇÃO FASE 3)
|
||||||
|
// ============================================================
|
||||||
|
app.get('/api/disciplinas', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const disciplinas = await getDisciplinas();
|
||||||
|
res.json({ disciplinas });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar disciplinas:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/disciplinas', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await insertDisciplina(req.body);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar disciplina:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/disciplinas/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await updateDisciplina(req.params.id, req.body);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao atualizar disciplina:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/disciplinas/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await deleteDisciplina(req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao deletar disciplina:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ROTAS DE TURMAS (MIGRAÇÃO FASE 3)
|
||||||
|
// ============================================================
|
||||||
|
app.get('/api/turmas', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const turmas = await getTurmas();
|
||||||
|
res.json({ turmas });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar turmas:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/turmas', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await insertTurma(req.body);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar turma:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/turmas/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await updateTurma(req.params.id, req.body);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao atualizar turma:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/turmas/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await deleteTurma(req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao deletar turma:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ROTAS DE FUNCIONÁRIOS (MIGRAÇÃO FASE 1)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
app.get('/api/categorias_funcionarios', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const categorias = await getCategoriasFuncionarios();
|
||||||
|
res.json({ categorias });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar categorias:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/categorias_funcionarios', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = req.body;
|
||||||
|
await insertCategoriaFuncionario(data);
|
||||||
|
res.json({ success: true, categoria: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar categoria:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/categorias_funcionarios/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { nome } = req.body;
|
||||||
|
await updateCategoriaFuncionario(id, nome);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao atualizar categoria:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/categorias_funcionarios/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
await deleteCategoriaFuncionario(id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao deletar categoria:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/funcionarios', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const funcionarios = await getFuncionarios();
|
||||||
|
res.json({ funcionarios });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar funcionarios:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/funcionarios', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = req.body;
|
||||||
|
await insertFuncionario(data);
|
||||||
|
res.json({ success: true, funcionario: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar funcionario:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/funcionarios/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const updateData = req.body;
|
||||||
|
await updateFuncionario(id, updateData);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao atualizar funcionario:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/funcionarios/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
await deleteFuncionario(id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao deletar funcionario:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Upload de Logo (MinIO em vez de Supabase Storage)
|
// Upload de Logo (MinIO em vez de Supabase Storage)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,177 @@ export async function deleteNotasManuaisAusentes(alunoId, notasManuaisRetidas) {
|
||||||
// Implementaremos a limpeza iterativamente na rota
|
// Implementaremos a limpeza iterativamente na rota
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HELPERS: cursos e turmas
|
||||||
|
// ============================================================
|
||||||
|
export async function getCursos() {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM cursos ORDER BY nome ASC');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertCurso(c) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO cursos (id, nome, duracao, duracao_meses, taxa_matricula, mensalidade, descricao, multa_percentual, juros_percentual)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||||
|
[c.id, c.nome, c.duracao || '', c.duracao_meses || 12, c.taxa_matricula || 0, c.mensalidade || 0, c.descricao || '', c.multa_percentual || 0, c.juros_percentual || 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCurso(id, updateData) {
|
||||||
|
const setClauses = [];
|
||||||
|
const values = [];
|
||||||
|
let i = 1;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(updateData)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setClauses.push(`${key} = $${i}`);
|
||||||
|
values.push(value);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setClauses.length === 0) return;
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE cursos SET ${setClauses.join(', ')} WHERE id = $${i}`,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCurso(id) {
|
||||||
|
await pool.query('DELETE FROM cursos WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTurmas() {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM turmas ORDER BY nome ASC');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertTurma(t) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO turmas (id, nome, curso_id, professor, horario, dia_semana, max_alunos, data_inicio, data_fim, horario_inicio_padrao, horario_fim_padrao)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||||
|
[t.id, t.nome, t.curso_id || null, t.professor || '', t.horario || '', t.dia_semana || null, t.max_alunos || 30, t.data_inicio || null, t.data_fim || null, t.horario_inicio_padrao || null, t.horario_fim_padrao || null]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTurma(id, updateData) {
|
||||||
|
const setClauses = [];
|
||||||
|
const values = [];
|
||||||
|
let i = 1;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(updateData)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setClauses.push(`${key} = $${i}`);
|
||||||
|
values.push(value);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setClauses.length === 0) return;
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE turmas SET ${setClauses.join(', ')} WHERE id = $${i}`,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTurma(id) {
|
||||||
|
await pool.query('DELETE FROM turmas WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDisciplinas() {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM disciplinas ORDER BY nome ASC');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertDisciplina(d) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO disciplinas (id, nome) VALUES ($1, $2)`,
|
||||||
|
[d.id, d.nome]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDisciplina(id, updateData) {
|
||||||
|
if (updateData.nome !== undefined) {
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE disciplinas SET nome = $1 WHERE id = $2`,
|
||||||
|
[updateData.nome, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDisciplina(id) {
|
||||||
|
await pool.query('DELETE FROM disciplinas WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HELPERS: funcionarios e categorias_funcionarios
|
||||||
|
// ============================================================
|
||||||
|
export async function getFuncionarios() {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM funcionarios ORDER BY nome ASC');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCategoriasFuncionarios() {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM categorias_funcionarios ORDER BY nome ASC');
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertFuncionario(f) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO funcionarios (id, nome, cpf, email, telefone, categoria_id, data_admissao)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
[f.id, f.nome, f.cpf, f.email, f.telefone, f.categoria_id, f.data_admissao || null]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateFuncionario(id, updateData) {
|
||||||
|
const setClauses = [];
|
||||||
|
const values = [];
|
||||||
|
let i = 1;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(updateData)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setClauses.push(`${key} = $${i}`);
|
||||||
|
values.push(value);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setClauses.length === 0) return;
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE funcionarios SET ${setClauses.join(', ')} WHERE id = $${i}`,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFuncionario(id) {
|
||||||
|
await pool.query('DELETE FROM funcionarios WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertCategoriaFuncionario(c) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO categorias_funcionarios (id, nome) VALUES ($1, $2)`,
|
||||||
|
[c.id, c.nome]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCategoriaFuncionario(id, nome) {
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE categorias_funcionarios SET nome = $1 WHERE id = $2`,
|
||||||
|
[nome, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCategoriaFuncionario(id) {
|
||||||
|
await pool.query('DELETE FROM categorias_funcionarios WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SINCRONIZAÇÃO: JSON -> TABELAS RELACIONAIS
|
// SINCRONIZAÇÃO: JSON -> TABELAS RELACIONAIS
|
||||||
// Garante que IDs do JSON existam nas tabelas para evitar erro de Foreign Key
|
// Garante que IDs do JSON existam nas tabelas para evitar erro de Foreign Key
|
||||||
|
|
@ -408,6 +579,34 @@ export async function syncJsonToRelationalTables() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.5 Sincronizar Categorias de Funcionários
|
||||||
|
if (data.employeeCategories && Array.isArray(data.employeeCategories)) {
|
||||||
|
for (const cat of data.employeeCategories) {
|
||||||
|
if (!cat.id || !cat.name) continue;
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO categorias_funcionarios (id, nome) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
|
||||||
|
[cat.id, cat.name]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.6 Sincronizar Funcionários
|
||||||
|
if (data.employees && Array.isArray(data.employees)) {
|
||||||
|
for (const emp of data.employees) {
|
||||||
|
if (!emp.id || !emp.name) continue;
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO funcionarios (id, nome, cpf, email, telefone, categoria_id, data_admissao)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
nome = EXCLUDED.nome, cpf = EXCLUDED.cpf, email = EXCLUDED.email,
|
||||||
|
telefone = EXCLUDED.telefone, categoria_id = EXCLUDED.categoria_id,
|
||||||
|
data_admissao = EXCLUDED.data_admissao`,
|
||||||
|
[emp.id, emp.name, emp.cpf || '', emp.email || '', emp.phone || '', emp.categoryId || null, emp.admissionDate || null]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Garantir colunas de refação em provas
|
// Garantir colunas de refação em provas
|
||||||
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS permitir_refacao BOOLEAN DEFAULT FALSE');
|
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS permitir_refacao BOOLEAN DEFAULT FALSE');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue