feat: Contratos 100% SQL-First + cache-buster em todos os módulos + persistência de aba ativa no F5

This commit is contained in:
Sidney 2026-05-26 08:42:13 -03:00
parent 15a7a9fef2
commit 1dc753c9c9
11 changed files with 191 additions and 83 deletions

View File

@ -25,9 +25,9 @@
| **Funcionários** | `employees`, `employeeCategories` | `funcionarios`, `categorias_funcionarios` | SQL Estruturado (`/api/funcionarios`) | SQL Estruturado (`POST`/`PUT`/`DELETE` `/api/funcionarios`) | Não aplicável | 🟢 **100% SQL-First** — Totalmente independente do JSON. | | **Funcionários** | `employees`, `employeeCategories` | `funcionarios`, `categorias_funcionarios` | SQL Estruturado (`/api/funcionarios`) | SQL Estruturado (`POST`/`PUT`/`DELETE` `/api/funcionarios`) | Não aplicável | 🟢 **100% SQL-First** — Totalmente independente do JSON. |
| **Boletim (Notas)** | `grades`, `periods` | `notas_boletim`, `periodos` | SQL Estruturado (`/api/notas`) | SQL Estruturado (`/api/notas`) | SQL Estruturado (Arithmetic Mean direto do SQL) | 🟢 **100% SQL-First** | | **Boletim (Notas)** | `grades`, `periods` | `notas_boletim`, `periodos` | SQL Estruturado (`/api/notas`) | SQL Estruturado (`/api/notas`) | SQL Estruturado (Arithmetic Mean direto do SQL) | 🟢 **100% SQL-First** |
| **Avaliações (Provas)** | `exams` | `provas`, `questoes_provas` | SQL Estruturado (`GET /api/provas` → estado `dbExams`) | SQL Estruturado (`POST`/`PUT`/`DELETE` `/api/provas` com sync de questões) | SQL Estruturado (Tabelas `provas` e `questoes_provas`) | 🟢 **100% SQL-First** — Zero chamadas `dbService.saveData` no frontend. Questões sincronizadas inline. Reverse-sync automático no backend. | | **Avaliações (Provas)** | `exams` | `provas`, `questoes_provas` | SQL Estruturado (`GET /api/provas` → estado `dbExams`) | SQL Estruturado (`POST`/`PUT`/`DELETE` `/api/provas` com sync de questões) | SQL Estruturado (Tabelas `provas` e `questoes_provas`) | 🟢 **100% SQL-First** — Zero chamadas `dbService.saveData` no frontend. Questões sincronizadas inline. Reverse-sync automático no backend. |
| **Frequências (Chamadas)** | `attendance` | `frequencias` | Híbrido (Lê contexto JSON do Manager) | SQL Estruturado (`POST /api/frequencias`) + Atualiza local | SQL Estruturado (Query direta no Postgres) | 🟡 **Híbrido / Em Transição** — Backend SQL-First; frontend do Manager sincroniza em duas vias. | | **Frequências (Chamadas)** | `attendance` | `frequencias` | SQL Estruturado (`GET /api/frequencias` → estado `dbAttendance`) | SQL Estruturado (`POST`/`PUT`/`DELETE` `/api/frequencias`) | SQL Estruturado (Query direta no Postgres) | 🟢 **100% SQL-First** — Zero chamadas `dbService.saveData` no frontend. Reverse-sync automático no backend. |
| **Aulas e Diários** | `lessons` | `aulas` | Híbrido (Lê contexto JSON do Manager) | SQL Estruturado (`/api/aulas/lote`) + Atualiza local | SQL Estruturado (Query direta no Postgres) | 🟡 **Híbrido / Em Transição** — Backend SQL-First; Frontend ainda acoplado ao contexto JSON. | | **Aulas e Diários** | `lessons` | `aulas` | SQL Estruturado (`GET /api/aulas` → estado `dbLessons`) | SQL Estruturado (`POST`/`DELETE` `/api/aulas/lote`) | SQL Estruturado (Query direta no Postgres) | 🟢 **100% SQL-First** — Zero chamadas `dbService.saveData` no frontend. Reverse-sync automático no backend. |
| **Contratos** | `contracts`, `contractTemplates` | `contratos`, `modelos_contrato` | Híbrido (Lê contexto JSON) | Híbrido (Escrita via JSON com trigger de sync no banco) | SQL Estruturado (Lê de `contratos`) | 🟡 **Híbrido / Em Transição** | | **Contratos** | `contracts`, `contractTemplates` | `contratos`, `modelos_contrato` | SQL Estruturado (`GET /api/contratos` e `GET /api/modelos-contrato`) | SQL Estruturado (`POST`/`PUT`/`DELETE`) | SQL Estruturado (Lê de `contratos`) | 🟢 **100% SQL-First** — Zero chamadas `dbService.saveData` no frontend. Reverse-sync no backend. |
| **Configurações Globais** | `profile`, `evolutionConfig`, `messageTemplates` | `configuracoes` | Pendente (Lê contexto JSON) | Pendente (Escreve via `dbService.saveData` no JSON) | Pendente (Lê do JSON) | 🔴 **Pendente** — Último bloco a ser migrado. | | **Configurações Globais** | `profile`, `evolutionConfig`, `messageTemplates` | `configuracoes` | Pendente (Lê contexto JSON) | Pendente (Escreve via `dbService.saveData` no JSON) | Pendente (Lê do JSON) | 🔴 **Pendente** — Último bloco a ser migrado. |
--- ---
@ -36,18 +36,15 @@
| Status | Qtd. Módulos | Módulos | | Status | Qtd. Módulos | Módulos |
|:---|:---:|:---| |:---|:---:|:---|
| 🟢 100% SQL-First | **5** | Alunos, Financeiro, Funcionários, Boletim, Avaliações | | 🟢 100% SQL-First | **8** | Alunos, Financeiro, Funcionários, Boletim, Avaliações, Frequências, Aulas, Contratos |
| 🟡 Híbrido | **3** | Frequências, Aulas, Contratos | | 🟡 Híbrido | **0** | Nenhum |
| 🔴 Pendente | **1** | Configurações Globais | | 🔴 Pendente | **1** | Configurações Globais |
--- ---
## Próximos Passos (Ordem Sugerida de Prioridade) ## Próximos Passos (Ordem Sugerida de Prioridade)
1. **Frequências (Chamadas)** — Frontend do Manager (`AttendanceQuery.tsx` / `LessonSchedule.tsx`) precisa ler do estado `dbAttendance` via SQL ao invés do contexto JSON `data.attendance`. 1. **Configurações Globais** — Criar tabela `configuracoes` no PostgreSQL e migrar `profile`, `evolutionConfig`, `messageTemplates` com rotas CRUD dedicadas.
2. **Aulas e Diários** — Frontend do Manager (`LessonSchedule.tsx`) precisa ler aulas do estado SQL (`dbLessons`) e eliminar a dependência de `data.lessons`.
3. **Contratos** — Frontend do Manager (`Contracts.tsx`) precisa ler/gravar via `/api/contratos` e eliminar `dbService.saveData`.
4. **Configurações Globais** — Criar tabela `configuracoes` no PostgreSQL e migrar `profile`, `evolutionConfig`, `messageTemplates` com rotas CRUD dedicadas.
--- ---

View File

@ -58,6 +58,38 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const [currentJustificationText, setCurrentJustificationText] = useState(''); const [currentJustificationText, setCurrentJustificationText] = useState('');
const [currentRecordForJustification, setCurrentRecordForJustification] = useState<Attendance | null>(null); const [currentRecordForJustification, setCurrentRecordForJustification] = useState<Attendance | null>(null);
const [dbAttendance, setDbAttendance] = useState<Attendance[]>(data.attendance || []);
const loadAttendance = async () => {
try {
const res = await fetch('/api/frequencias');
if (res.ok) {
const json = await res.json();
if (json.frequencias) {
setDbAttendance(json.frequencias.map((r: any) => ({
id: r.id,
studentId: r.studentId || r.aluno_id,
classId: r.classId || r.turma_id,
lessonId: r.lessonId || r.aula_id,
date: r.date || r.data,
photo: r.photo || r.foto || r.foto_url,
verified: r.verified ?? r.verificado ?? false,
type: r.type || r.tipo || 'presence',
justification: r.justification || r.justificativa,
justificationAccepted: r.justificationAccepted ?? r.justificativa_aceita ?? false,
createdAt: r.createdAt || r.created_at
})));
}
}
} catch (e) {
console.error('Erro ao carregar frequencias do SQL:', e);
}
};
useEffect(() => {
loadAttendance();
}, []);
// Helper para normalizar URLs de fotos (vacina contra cache antigo) // 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 '';
@ -72,8 +104,8 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
return url; return url;
}; };
const toggleAttendanceStatus = (record: any) => { const toggleAttendanceStatus = async (record: any) => {
let updatedAttendance = [...(data.attendance || [])]; let updatedAttendance = [...dbAttendance];
if (record.isVirtual) { if (record.isVirtual) {
// Ação do botão do Admin: criar o registro real a partir do virtual // Ação do botão do Admin: criar o registro real a partir do virtual
@ -106,10 +138,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
if (existingIdx >= 0) { if (existingIdx >= 0) {
updatedAttendance[existingIdx] = { ...updatedAttendance[existingIdx], type: newType, justification: undefined, justificationAccepted: undefined }; updatedAttendance[existingIdx] = { ...updatedAttendance[existingIdx], type: newType, justification: undefined, justificationAccepted: undefined };
fetch(`/api/frequencias/${updatedAttendance[existingIdx].id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedAttendance[existingIdx]) }); await fetch(`/api/frequencias/${updatedAttendance[existingIdx].id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedAttendance[existingIdx]) });
} else { } else {
updatedAttendance.push(newRecord); updatedAttendance.push(newRecord);
fetch('/api/frequencias', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newRecord) }); await fetch('/api/frequencias', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newRecord) });
} }
} else { } else {
// Toggle existing record // Toggle existing record
@ -118,14 +150,14 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
updatedAttendance = updatedAttendance.map(a => updatedAttendance = updatedAttendance.map(a =>
a.id === record.id ? modifiedRecord : a a.id === record.id ? modifiedRecord : a
); );
fetch(`/api/frequencias/${record.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) }); await fetch(`/api/frequencias/${record.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) });
} }
updateData({ attendance: updatedAttendance }); setDbAttendance(updatedAttendance);
showAlert('Sucesso', 'Status de frequência atualizado com sucesso.', 'success'); showAlert('Sucesso', 'Status de frequência atualizado com sucesso.', 'success');
}; };
const handleDeleteAttachmentRecord = () => { const handleDeleteAttachmentRecord = async () => {
if (!attendanceForAttachment || !attendanceForAttachment.justification) return; if (!attendanceForAttachment || !attendanceForAttachment.justification) return;
try { try {
@ -134,14 +166,14 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
delete parsed.arquivo; delete parsed.arquivo;
const updatedJustification = JSON.stringify(parsed); const updatedJustification = JSON.stringify(parsed);
const updatedAttendance = (data.attendance || []).map(a => const updatedAttendance = dbAttendance.map(a =>
a.id === attendanceForAttachment.id ? { ...a, justification: updatedJustification } : a a.id === attendanceForAttachment.id ? { ...a, justification: updatedJustification } : a
); );
const modifiedRecord = updatedAttendance.find(a => a.id === attendanceForAttachment.id); const modifiedRecord = updatedAttendance.find(a => a.id === attendanceForAttachment.id);
fetch(`/api/frequencias/${attendanceForAttachment.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) }); await fetch(`/api/frequencias/${attendanceForAttachment.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) });
updateData({ attendance: updatedAttendance }); setDbAttendance(updatedAttendance);
setViewingAttachment(null); setViewingAttachment(null);
setAttendanceForAttachment(null); setAttendanceForAttachment(null);
showAlert('Sucesso', 'Arquivo removido com sucesso.', 'success'); showAlert('Sucesso', 'Arquivo removido com sucesso.', 'success');
@ -171,7 +203,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
}, 400); }, 400);
}; };
const handleAddAbsence = () => { const handleAddAbsence = async () => {
if (!absenceStudentId || !absenceJustification || !absenceLessonId) { if (!absenceStudentId || !absenceJustification || !absenceLessonId) {
showAlert('Atenção', "⚠️ Por favor, preencha todos os campos da justificativa.", 'warning'); showAlert('Atenção', "⚠️ Por favor, preencha todos os campos da justificativa.", 'warning');
return; return;
@ -190,12 +222,12 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
} }
// Check if there is already a record for this lesson specifically // Check if there is already a record for this lesson specifically
const existingIndex = (data.attendance || []).findIndex(a => const existingIndex = dbAttendance.findIndex(a =>
a.studentId === absenceStudentId && a.studentId === absenceStudentId &&
((a as any).lessonId === lesson.id || a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) ((a as any).lessonId === lesson.id || a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`)
); );
let updatedAttendance = [...(data.attendance || [])]; let updatedAttendance = [...dbAttendance];
if (existingIndex >= 0) { if (existingIndex >= 0) {
updatedAttendance[existingIndex] = { updatedAttendance[existingIndex] = {
@ -206,7 +238,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
verified: true, verified: true,
lessonId: lesson.id as any lessonId: lesson.id as any
}; };
fetch(`/api/frequencias/${updatedAttendance[existingIndex].id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedAttendance[existingIndex]) }); await fetch(`/api/frequencias/${updatedAttendance[existingIndex].id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedAttendance[existingIndex]) });
} else { } else {
const newAbsence: Attendance = { const newAbsence: Attendance = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
@ -220,11 +252,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
...(lesson ? { lessonId: lesson.id } : {}) as any ...(lesson ? { lessonId: lesson.id } : {}) as any
}; };
updatedAttendance.push(newAbsence); updatedAttendance.push(newAbsence);
fetch('/api/frequencias', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newAbsence) }); await fetch('/api/frequencias', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newAbsence) });
} }
updateData({ attendance: updatedAttendance }); setDbAttendance(updatedAttendance);
dbService.saveData({ ...data, attendance: updatedAttendance });
setAbsenceStudentId(''); setAbsenceStudentId('');
setAbsenceJustification(''); setAbsenceJustification('');
@ -246,7 +277,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
doc.text(`Data: ${new Date(selectedDate).toLocaleDateString()}`, 14, startY + 18); doc.text(`Data: ${new Date(selectedDate).toLocaleDateString()}`, 14, startY + 18);
doc.text(`Turma: ${classObj.name}`, 14, startY + 24); doc.text(`Turma: ${classObj.name}`, 14, startY + 24);
const classAttendance = (data.attendance || []).filter(record => const classAttendance = dbAttendance.filter(record =>
record.classId === classObj.id && record.date.startsWith(selectedDate) record.classId === classObj.id && record.date.startsWith(selectedDate)
); );
@ -310,7 +341,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
<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(classObj => { {data.classes.map(classObj => {
const classStudents = data.students.filter(s => s.classId === classObj.id && s.status === 'active'); const classStudents = data.students.filter(s => s.classId === classObj.id && s.status === 'active');
const attendanceCount = (data.attendance || []).filter(a => a.classId === classObj.id && a.date.startsWith(selectedDate)).length; const attendanceCount = dbAttendance.filter(a => a.classId === classObj.id && a.date.startsWith(selectedDate)).length;
const course = data.courses.find(c => c.id === classObj.courseId); const course = data.courses.find(c => c.id === classObj.courseId);
return ( return (
@ -395,7 +426,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
} }
return classStudents.map(student => { return classStudents.map(student => {
const studentActualRecords = (data.attendance || []).filter(a => a.studentId === student.id && a.classId === selectedClass.id); const studentActualRecords = dbAttendance.filter(a => a.studentId === student.id && a.classId === selectedClass.id);
const classLessonsRaw = (data.lessons || []).filter(l => l.classId === selectedClass.id && l.status !== 'cancelled'); const classLessonsRaw = (data.lessons || []).filter(l => l.classId === selectedClass.id && l.status !== 'cancelled');
const deduplicatedLessons = classLessonsRaw.filter((lesson, index, self) => const deduplicatedLessons = classLessonsRaw.filter((lesson, index, self) =>
@ -510,10 +541,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const now = new Date(); const now = new Date();
const studentClassIds = new Set([ const studentClassIds = new Set([
selectedClass.id, selectedClass.id,
...(data.attendance || []).filter(a => a.studentId === selectedStudent.id).map(a => a.classId) ...dbAttendance.filter(a => a.studentId === selectedStudent.id).map(a => a.classId)
].filter(Boolean)); ].filter(Boolean));
const actualRecords = (data.attendance || []) const actualRecords = dbAttendance
.filter(a => a.studentId === selectedStudent.id); .filter(a => a.studentId === selectedStudent.id);
const classLessonsRaw = (data.lessons || []) const classLessonsRaw = (data.lessons || [])
@ -745,12 +776,11 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
{hasPendingJustification && ( {hasPendingJustification && (
<button <button
onClick={() => { onClick={async () => {
const updated = (data.attendance || []).map(a => a.id === record.id ? { ...a, justificationAccepted: true } : a); const updated = dbAttendance.map(a => a.id === record.id ? { ...a, justificationAccepted: true } : a);
const modifiedRecord = updated.find(a => a.id === record.id); const modifiedRecord = updated.find(a => a.id === record.id);
fetch(`/api/frequencias/${record.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) }); await fetch(`/api/frequencias/${record.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) });
updateData({ attendance: updated }); setDbAttendance(updated);
dbService.saveData({ ...data, attendance: updated });
showAlert('Sucesso', 'Justificativa aceita com sucesso.', 'success'); showAlert('Sucesso', 'Justificativa aceita com sucesso.', 'success');
}} }}
className="text-[10px] px-2 py-1.5 bg-indigo-600 text-white font-bold rounded hover:bg-indigo-700 transition-colors" className="text-[10px] px-2 py-1.5 bg-indigo-600 text-white font-bold rounded hover:bg-indigo-700 transition-colors"

View File

@ -45,8 +45,8 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
const loadData = async () => { const loadData = async () => {
try { try {
const [resC, resT] = await Promise.all([ const [resC, resT] = await Promise.all([
fetch('/api/contratos'), fetch(`/api/contratos?t=${Date.now()}`),
fetch('/api/modelos-contrato') fetch(`/api/modelos-contrato?t=${Date.now()}`)
]); ]);
if (resC.ok) { if (resC.ok) {
const json = await resC.json(); const json = await resC.json();
@ -172,9 +172,6 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
body: JSON.stringify(formData) body: JSON.stringify(formData)
}).then(() => loadData()); }).then(() => loadData());
updateData({
contracts: dbContracts.map(c => c.id === (formData as any).id ? { ...c, ...formData } : c)
});
closeModal(); closeModal();
return; return;
} }
@ -191,7 +188,6 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
body: JSON.stringify(newContract) body: JSON.stringify(newContract)
}).then(() => loadData()); }).then(() => loadData());
updateData({ contracts: [...dbContracts, newContract] });
closeModal(); closeModal();
}; };
@ -247,7 +243,6 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
body: JSON.stringify({ ...templateFormData, id: updatedTemplates.find(t=>t.name===templateFormData.name)?.id || crypto.randomUUID() }) body: JSON.stringify({ ...templateFormData, id: updatedTemplates.find(t=>t.name===templateFormData.name)?.id || crypto.randomUUID() })
}).then(() => loadData()); }).then(() => loadData());
} }
updateData({ contractTemplates: updatedTemplates });
closeTemplateModal(); closeTemplateModal();
}; };
@ -276,7 +271,7 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
'Excluir Contrato', 'Excluir Contrato',
'Tem certeza que deseja excluir este contrato?', 'Tem certeza que deseja excluir este contrato?',
() => { () => {
updateData({ contracts: dbContracts.filter(c => c.id !== id) }); fetch(`/api/contratos/${id}`, { method: 'DELETE' }).then(() => loadData());
} }
); );
}; };
@ -286,7 +281,7 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
'Excluir Modelo', 'Excluir Modelo',
'Tem certeza que deseja excluir este modelo de contrato?', 'Tem certeza que deseja excluir este modelo de contrato?',
() => { () => {
updateData({ contractTemplates: dbTemplates.filter(t => t.id !== id) }); fetch(`/api/modelos-contrato/${id}`, { method: 'DELETE' }).then(() => loadData());
} }
); );
}; };

View File

@ -31,8 +31,8 @@ const Employees: React.FC = () => {
try { try {
setIsLoadingData(true); setIsLoadingData(true);
const [empRes, catRes] = await Promise.all([ const [empRes, catRes] = await Promise.all([
fetch('/api/funcionarios'), fetch(`/api/funcionarios?t=${Date.now()}`),
fetch('/api/categorias_funcionarios') fetch(`/api/categorias_funcionarios?t=${Date.now()}`)
]); ]);
const empData = await empRes.json(); const empData = await empRes.json();
const catData = await catRes.json(); const catData = await catRes.json();

View File

@ -27,7 +27,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
const loadExams = async () => { const loadExams = async () => {
try { try {
const res = await fetch('/api/provas'); const res = await fetch(`/api/provas?t=${Date.now()}`);
if (res.ok) { if (res.ok) {
const { provas } = await res.json(); const { provas } = await res.json();
setDbExams(provas.map((p: any) => ({ setDbExams(provas.map((p: any) => ({

View File

@ -178,7 +178,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
const fetchPostgresPayments = async () => { const fetchPostgresPayments = async () => {
try { try {
const resp = await fetch('/api/admin/cobrancas'); const resp = await fetch(`/api/admin/cobrancas?t=${Date.now()}`);
if (resp.ok) { if (resp.ok) {
const records = await resp.json(); const records = await resp.json();
const normalized = (records || []).map((r: any) => { const normalized = (records || []).map((r: any) => {
@ -260,7 +260,7 @@ const Finance: React.FC<FinanceProps> = ({ data, updateData }) => {
setIsFetchingSupabase(true); setIsFetchingSupabase(true);
setSelectedSupabaseRows([]); setSelectedSupabaseRows([]);
try { try {
const resp = await fetch('/api/admin/cobrancas'); const resp = await fetch(`/api/admin/cobrancas?t=${Date.now()}`);
if (!resp.ok) throw new Error('API fetch failed'); if (!resp.ok) throw new Error('API fetch failed');
const records = await resp.json(); const records = await resp.json();
setSupabaseRecords(records || []); setSupabaseRecords(records || []);

View File

@ -17,7 +17,7 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const [dbLessons, setDbLessons] = useState<Lesson[]>([]); const [dbLessons, setDbLessons] = useState<Lesson[]>([]);
const loadLessons = async () => { const loadLessons = async () => {
try { try {
const res = await fetch(`/api/aulas?turma_id=${classObj.id}`); const res = await fetch(`/api/aulas?turma_id=${classObj.id}&t=${Date.now()}`);
if (res.ok) { if (res.ok) {
const json = await res.json(); const json = await res.json();
setDbLessons(json.aulas || []); setDbLessons(json.aulas || []);
@ -167,8 +167,6 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
body: JSON.stringify({ aulas: newLessons }) body: JSON.stringify({ aulas: newLessons })
}).then(() => loadLessons()); }).then(() => loadLessons());
const updatedLessons = [...(data.lessons || []), ...newLessons];
// Notificar alunos sobre novas aulas extras geradas // Notificar alunos sobre novas aulas extras geradas
const datesList = newLessons.map(l => new Date(l.date + 'T12:00:00Z').toLocaleDateString('pt-BR')).join(', '); const datesList = newLessons.map(l => new Date(l.date + 'T12:00:00Z').toLocaleDateString('pt-BR')).join(', ');
const notifMsg = `Novas aulas extras foram agendadas para os dias: ${datesList} (${startTime} às ${endTime}).`; const notifMsg = `Novas aulas extras foram agendadas para os dias: ${datesList} (${startTime} às ${endTime}).`;
@ -177,8 +175,8 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const newNotifs = notifyLessonAction('Aulas Extras Agendadas', notifMsg, waMsg); const newNotifs = notifyLessonAction('Aulas Extras Agendadas', notifMsg, waMsg);
const updatedNotifications = [...(data.notifications || []), ...newNotifs]; const updatedNotifications = [...(data.notifications || []), ...newNotifs];
updateData({ lessons: updatedLessons, notifications: updatedNotifications }); updateData({ notifications: updatedNotifications });
dbService.saveData({ ...data, lessons: updatedLessons, notifications: updatedNotifications }); dbService.saveData({ ...data, notifications: updatedNotifications });
setShowGenerateModal(false); setShowGenerateModal(false);
@ -317,8 +315,8 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons }) body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons()); }).then(() => loadLessons());
updateData({ lessons: [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedLessons], notifications: updatedNotifications }); updateData({ notifications: updatedNotifications });
await dbService.saveData({ ...data, lessons: [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedLessons], notifications: updatedNotifications }); await dbService.saveData({ ...data, notifications: updatedNotifications });
@ -336,7 +334,7 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const handleUncancelLesson = async (lesson: Lesson) => { const handleUncancelLesson = async (lesson: Lesson) => {
setIsClosing(true); setIsClosing(true);
const updatedLessons: Lesson[] = (data.lessons || []).map(l => const updatedLessons: Lesson[] = dbLessons.map(l =>
l.id === lesson.id ? { ...l, status: 'scheduled', cancelReason: undefined } : l l.id === lesson.id ? { ...l, status: 'scheduled', cancelReason: undefined } : l
); );
fetch('/api/aulas/lote', { fetch('/api/aulas/lote', {
@ -344,8 +342,6 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons }) body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons()); }).then(() => loadLessons());
updateData({ lessons: [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedLessons] });
await dbService.saveData({ ...data, lessons: [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedLessons] });
setTimeout(() => { setTimeout(() => {
setShowLessonDetail(null); setShowLessonDetail(null);
@ -397,10 +393,8 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
body: JSON.stringify({ aulas: updatedClassLessons }) body: JSON.stringify({ aulas: updatedClassLessons })
}).then(() => loadLessons()); }).then(() => loadLessons());
const allUpdatedLessons = [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedClassLessons]; updateData({ notifications: updatedNotifications, attendance: updatedAttendance });
await dbService.saveData({ ...data, notifications: updatedNotifications, attendance: updatedAttendance });
updateData({ lessons: allUpdatedLessons, notifications: updatedNotifications, attendance: updatedAttendance });
await dbService.saveData({ ...data, lessons: allUpdatedLessons, notifications: updatedNotifications, attendance: updatedAttendance });
setTimeout(() => { setTimeout(() => {
setShowLessonDetail(null); setShowLessonDetail(null);
@ -415,14 +409,17 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const handleCancelAllFuture = () => { const handleCancelAllFuture = () => {
showConfirm('Cancelar Cronograma', 'Deseja realmente cancelar TODAS as aulas futuras não realizadas? Não haverá reposição e a ação atualizará todas para Cancelada.', async () => { showConfirm('Cancelar Cronograma', 'Deseja realmente cancelar TODAS as aulas futuras não realizadas? Não haverá reposição e a ação atualizará todas para Cancelada.', async () => {
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
const updatedLessons = (data.lessons || []).map(l => { const updatedLessons = dbLessons.map(l => {
if (l.classId === classObj.id && l.status === 'scheduled' && l.date >= today) { if (l.status === 'scheduled' && l.date >= today) {
return { ...l, status: 'cancelled', cancelReason: 'Cancelamento Geral de Cronograma' }; return { ...l, status: 'cancelled', cancelReason: 'Cancelamento Geral de Cronograma' };
} }
return l; return l;
}); });
updateData({ lessons: updatedLessons as Lesson[] }); fetch('/api/aulas/lote', {
await dbService.saveData({ ...data, lessons: updatedLessons as Lesson[] }); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons());
showAlert('Sucesso', 'Cronograma futuro cancelado.', 'success'); showAlert('Sucesso', 'Cronograma futuro cancelado.', 'success');
}); });
}; };
@ -430,14 +427,17 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const handleUncancelAllFuture = () => { const handleUncancelAllFuture = () => {
showConfirm('Reativar Cronograma', 'Deseja realmente reativar TODAS as aulas futuras que estavam canceladas?', async () => { showConfirm('Reativar Cronograma', 'Deseja realmente reativar TODAS as aulas futuras que estavam canceladas?', async () => {
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
const updatedLessons = (data.lessons || []).map(l => { const updatedLessons = dbLessons.map(l => {
if (l.classId === classObj.id && l.status === 'cancelled' && l.date >= today) { if (l.status === 'cancelled' && l.date >= today) {
return { ...l, status: 'scheduled', cancelReason: undefined }; return { ...l, status: 'scheduled', cancelReason: undefined };
} }
return l; return l;
}); });
updateData({ lessons: updatedLessons as Lesson[] }); fetch('/api/aulas/lote', {
await dbService.saveData({ ...data, lessons: updatedLessons as Lesson[] }); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons());
showAlert('Sucesso', 'Cronograma futuro reativado com sucesso.', 'success'); showAlert('Sucesso', 'Cronograma futuro reativado com sucesso.', 'success');
}); });
}; };
@ -454,9 +454,6 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
}); });
} }
const updatedLessons = (data.lessons || []).filter(l => l.classId !== classObj.id);
updateData({ lessons: updatedLessons });
await dbService.saveData({ ...data, lessons: updatedLessons });
await loadLessons(); await loadLessons();
showAlert('Sucesso', 'Cronograma completo excluído.', 'success'); showAlert('Sucesso', 'Cronograma completo excluído.', 'success');
}); });

View File

@ -44,7 +44,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
const loadStudents = async () => { const loadStudents = async () => {
try { try {
const res = await fetch('/api/alunos'); const res = await fetch(`/api/alunos?t=${Date.now()}`);
if (res.ok) { if (res.ok) {
const json = await res.json(); const json = await res.json();
setDbStudents(json.alunos || []); setDbStudents(json.alunos || []);

View File

@ -29,7 +29,12 @@ const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isCheckingAuth, setIsCheckingAuth] = useState(true); const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [currentView, setCurrentView] = useState<View>(View.Dashboard); const [currentView, setCurrentView] = useState<View>(() => {
return (localStorage.getItem('manager_active_tab') as View) || View.Dashboard;
});
useEffect(() => {
localStorage.setItem('manager_active_tab', currentView);
}, [currentView]);
const [deepLinkStudentId, setDeepLinkStudentId] = useState<string | null>(null); const [deepLinkStudentId, setDeepLinkStudentId] = useState<string | null>(null);
const [deepLinkClassId, setDeepLinkClassId] = useState<string | null>(null); const [deepLinkClassId, setDeepLinkClassId] = useState<string | null>(null);
// Initial load from LocalStorage for speed (fallback), then IDB // Initial load from LocalStorage for speed (fallback), then IDB

View File

@ -607,26 +607,68 @@ app.get('/api/modelos-contrato', async (req, res) => {
try { res.json({ modelos: await getModelosContrato() }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try { res.json({ modelos: await getModelosContrato() }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.post('/api/modelos-contrato', async (req, res) => { app.post('/api/modelos-contrato', async (req, res) => {
try { await insertModeloContrato(req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try {
await insertModeloContrato(req.body);
const appData = await getSchoolData();
appData.contractTemplates = await getModelosContrato();
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.put('/api/modelos-contrato/:id', async (req, res) => { app.put('/api/modelos-contrato/:id', async (req, res) => {
try { await updateModeloContrato(req.params.id, req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try {
await updateModeloContrato(req.params.id, req.body);
const appData = await getSchoolData();
appData.contractTemplates = await getModelosContrato();
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.delete('/api/modelos-contrato/:id', async (req, res) => { app.delete('/api/modelos-contrato/:id', async (req, res) => {
try { await deleteModeloContrato(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try {
await deleteModeloContrato(req.params.id);
const appData = await getSchoolData();
appData.contractTemplates = await getModelosContrato();
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.get('/api/contratos', async (req, res) => { app.get('/api/contratos', async (req, res) => {
try { res.json({ contratos: await getContratos() }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try { res.json({ contratos: await getContratos() }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.post('/api/contratos', async (req, res) => { app.post('/api/contratos', async (req, res) => {
try { await insertContrato(req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try {
await insertContrato(req.body);
const appData = await getSchoolData();
appData.contracts = await getContratos();
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.put('/api/contratos/:id', async (req, res) => { app.put('/api/contratos/:id', async (req, res) => {
try { await updateContrato(req.params.id, req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try {
await updateContrato(req.params.id, req.body);
const appData = await getSchoolData();
appData.contracts = await getContratos();
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
app.delete('/api/contratos/:id', async (req, res) => { app.delete('/api/contratos/:id', async (req, res) => {
try { await deleteContrato(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); } try {
await deleteContrato(req.params.id);
const appData = await getSchoolData();
appData.contracts = await getContratos();
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: 'Erro' }); }
}); });
// ============================================================ // ============================================================
@ -647,6 +689,14 @@ app.post('/api/aulas/lote', async (req, res) => {
try { try {
const { aulas } = req.body; const { aulas } = req.body;
await insertAulas(aulas); await insertAulas(aulas);
// Reverse sync to legacy JSON
const appData = await getSchoolData();
const dbAulas = await getAllAulas();
appData.lessons = dbAulas;
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao inserir aulas em lote:', error); console.error('Erro ao inserir aulas em lote:', error);
@ -658,6 +708,14 @@ app.delete('/api/aulas/lote', async (req, res) => {
try { try {
const { ids } = req.body; const { ids } = req.body;
await deleteAulas(ids); await deleteAulas(ids);
// Reverse sync to legacy JSON
const appData = await getSchoolData();
const dbAulas = await getAllAulas();
appData.lessons = dbAulas;
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao deletar aulas em lote:', error); console.error('Erro ao deletar aulas em lote:', error);
@ -681,6 +739,14 @@ app.get('/api/frequencias', async (req, res) => {
app.post('/api/frequencias', async (req, res) => { app.post('/api/frequencias', async (req, res) => {
try { try {
await insertFrequencia(req.body); await insertFrequencia(req.body);
// Reverse sync to legacy JSON
const appData = await getSchoolData();
const dbFrequencias = await getFrequencias();
appData.attendance = dbFrequencias;
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao inserir frequencia:', error); console.error('Erro ao inserir frequencia:', error);
@ -691,6 +757,14 @@ app.post('/api/frequencias', async (req, res) => {
app.put('/api/frequencias/:id', async (req, res) => { app.put('/api/frequencias/:id', async (req, res) => {
try { try {
await updateFrequencia(req.params.id, req.body); await updateFrequencia(req.params.id, req.body);
// Reverse sync to legacy JSON
const appData = await getSchoolData();
const dbFrequencias = await getFrequencias();
appData.attendance = dbFrequencias;
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao atualizar frequencia:', error); console.error('Erro ao atualizar frequencia:', error);
@ -701,6 +775,14 @@ app.put('/api/frequencias/:id', async (req, res) => {
app.delete('/api/frequencias/:id', async (req, res) => { app.delete('/api/frequencias/:id', async (req, res) => {
try { try {
await deleteFrequencia(req.params.id); await deleteFrequencia(req.params.id);
// Reverse sync to legacy JSON
const appData = await getSchoolData();
const dbFrequencias = await getFrequencias();
appData.attendance = dbFrequencias;
appData.lastUpdated = new Date().toISOString();
await saveSchoolData(appData);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
console.error('Erro ao deletar frequencia:', error); console.error('Erro ao deletar frequencia:', error);

View File

@ -593,6 +593,8 @@ app.post('/api/portal/frequencia/justificar', authMiddleware, upload.single('arq
const submittedAt = new Date().toISOString(); const submittedAt = new Date().toISOString();
let recordIndex = attendance.findIndex(a => a.studentId === req.user.studentId && a.date === fullDateStr);
if (recordIndex !== -1) { if (recordIndex !== -1) {
const existing = attendance[recordIndex]; const existing = attendance[recordIndex];
if (existing.type === 'presence') return res.status(400).json({ error: 'Não é possível justificar uma presença' }); if (existing.type === 'presence') return res.status(400).json({ error: 'Não é possível justificar uma presença' });