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. |
| **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. |
| **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. |
| **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. |
| **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** |
| **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` | 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` | 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. |
---
@ -36,18 +36,15 @@
| Status | Qtd. Módulos | Módulos |
|:---|:---:|:---|
| 🟢 100% SQL-First | **5** | Alunos, Financeiro, Funcionários, Boletim, Avaliações |
| 🟡 Híbrido | **3** | Frequências, Aulas, Contratos |
| 🟢 100% SQL-First | **8** | Alunos, Financeiro, Funcionários, Boletim, Avaliações, Frequências, Aulas, Contratos |
| 🟡 Híbrido | **0** | Nenhum |
| 🔴 Pendente | **1** | Configurações Globais |
---
## 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`.
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.
1. **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 [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)
const normalizePhotoUrl = (url?: string) => {
if (!url || typeof url !== 'string') return '';
@ -72,8 +104,8 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
return url;
};
const toggleAttendanceStatus = (record: any) => {
let updatedAttendance = [...(data.attendance || [])];
const toggleAttendanceStatus = async (record: any) => {
let updatedAttendance = [...dbAttendance];
if (record.isVirtual) {
// 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) {
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 {
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 {
// Toggle existing record
@ -118,14 +150,14 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
updatedAttendance = updatedAttendance.map(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');
};
const handleDeleteAttachmentRecord = () => {
const handleDeleteAttachmentRecord = async () => {
if (!attendanceForAttachment || !attendanceForAttachment.justification) return;
try {
@ -134,14 +166,14 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
delete parsed.arquivo;
const updatedJustification = JSON.stringify(parsed);
const updatedAttendance = (data.attendance || []).map(a =>
const updatedAttendance = dbAttendance.map(a =>
a.id === attendanceForAttachment.id ? { ...a, justification: updatedJustification } : a
);
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);
setAttendanceForAttachment(null);
showAlert('Sucesso', 'Arquivo removido com sucesso.', 'success');
@ -171,7 +203,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
}, 400);
};
const handleAddAbsence = () => {
const handleAddAbsence = async () => {
if (!absenceStudentId || !absenceJustification || !absenceLessonId) {
showAlert('Atenção', "⚠️ Por favor, preencha todos os campos da justificativa.", 'warning');
return;
@ -190,12 +222,12 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
}
// 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 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) {
updatedAttendance[existingIndex] = {
@ -206,7 +238,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
verified: true,
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 {
const newAbsence: Attendance = {
id: crypto.randomUUID(),
@ -220,11 +252,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
...(lesson ? { lessonId: lesson.id } : {}) as any
};
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 });
dbService.saveData({ ...data, attendance: updatedAttendance });
setDbAttendance(updatedAttendance);
setAbsenceStudentId('');
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(`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)
);
@ -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">
{data.classes.map(classObj => {
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);
return (
@ -395,7 +426,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
}
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 deduplicatedLessons = classLessonsRaw.filter((lesson, index, self) =>
@ -510,10 +541,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const now = new Date();
const studentClassIds = new Set([
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));
const actualRecords = (data.attendance || [])
const actualRecords = dbAttendance
.filter(a => a.studentId === selectedStudent.id);
const classLessonsRaw = (data.lessons || [])
@ -745,12 +776,11 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
{hasPendingJustification && (
<button
onClick={() => {
const updated = (data.attendance || []).map(a => a.id === record.id ? { ...a, justificationAccepted: true } : a);
onClick={async () => {
const updated = dbAttendance.map(a => a.id === record.id ? { ...a, justificationAccepted: true } : a);
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) });
updateData({ attendance: updated });
dbService.saveData({ ...data, attendance: updated });
await fetch(`/api/frequencias/${record.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) });
setDbAttendance(updated);
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"

View File

@ -45,8 +45,8 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
const loadData = async () => {
try {
const [resC, resT] = await Promise.all([
fetch('/api/contratos'),
fetch('/api/modelos-contrato')
fetch(`/api/contratos?t=${Date.now()}`),
fetch(`/api/modelos-contrato?t=${Date.now()}`)
]);
if (resC.ok) {
const json = await resC.json();
@ -172,9 +172,6 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
body: JSON.stringify(formData)
}).then(() => loadData());
updateData({
contracts: dbContracts.map(c => c.id === (formData as any).id ? { ...c, ...formData } : c)
});
closeModal();
return;
}
@ -191,7 +188,6 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
body: JSON.stringify(newContract)
}).then(() => loadData());
updateData({ contracts: [...dbContracts, newContract] });
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() })
}).then(() => loadData());
}
updateData({ contractTemplates: updatedTemplates });
closeTemplateModal();
};
@ -276,7 +271,7 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
'Excluir 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',
'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 {
setIsLoadingData(true);
const [empRes, catRes] = await Promise.all([
fetch('/api/funcionarios'),
fetch('/api/categorias_funcionarios')
fetch(`/api/funcionarios?t=${Date.now()}`),
fetch(`/api/categorias_funcionarios?t=${Date.now()}`)
]);
const empData = await empRes.json();
const catData = await catRes.json();

View File

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

View File

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

View File

@ -17,7 +17,7 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const [dbLessons, setDbLessons] = useState<Lesson[]>([]);
const loadLessons = async () => {
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) {
const json = await res.json();
setDbLessons(json.aulas || []);
@ -167,8 +167,6 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
body: JSON.stringify({ aulas: newLessons })
}).then(() => loadLessons());
const updatedLessons = [...(data.lessons || []), ...newLessons];
// Notificar alunos sobre novas aulas extras geradas
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}).`;
@ -177,8 +175,8 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const newNotifs = notifyLessonAction('Aulas Extras Agendadas', notifMsg, waMsg);
const updatedNotifications = [...(data.notifications || []), ...newNotifs];
updateData({ lessons: updatedLessons, notifications: updatedNotifications });
dbService.saveData({ ...data, lessons: updatedLessons, notifications: updatedNotifications });
updateData({ notifications: updatedNotifications });
dbService.saveData({ ...data, notifications: updatedNotifications });
setShowGenerateModal(false);
@ -317,8 +315,8 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons());
updateData({ lessons: [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedLessons], notifications: updatedNotifications });
await dbService.saveData({ ...data, lessons: [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedLessons], notifications: updatedNotifications });
updateData({ 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) => {
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
);
fetch('/api/aulas/lote', {
@ -344,8 +342,6 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons })
}).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(() => {
setShowLessonDetail(null);
@ -397,10 +393,8 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
body: JSON.stringify({ aulas: updatedClassLessons })
}).then(() => loadLessons());
const allUpdatedLessons = [...(data.lessons || []).filter(dl => dl.classId !== classObj.id), ...updatedClassLessons];
updateData({ lessons: allUpdatedLessons, notifications: updatedNotifications, attendance: updatedAttendance });
await dbService.saveData({ ...data, lessons: allUpdatedLessons, notifications: updatedNotifications, attendance: updatedAttendance });
updateData({ notifications: updatedNotifications, attendance: updatedAttendance });
await dbService.saveData({ ...data, notifications: updatedNotifications, attendance: updatedAttendance });
setTimeout(() => {
setShowLessonDetail(null);
@ -415,14 +409,17 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
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 () => {
const today = new Date().toISOString().split('T')[0];
const updatedLessons = (data.lessons || []).map(l => {
if (l.classId === classObj.id && l.status === 'scheduled' && l.date >= today) {
const updatedLessons = dbLessons.map(l => {
if (l.status === 'scheduled' && l.date >= today) {
return { ...l, status: 'cancelled', cancelReason: 'Cancelamento Geral de Cronograma' };
}
return l;
});
updateData({ lessons: updatedLessons as Lesson[] });
await dbService.saveData({ ...data, lessons: updatedLessons as Lesson[] });
fetch('/api/aulas/lote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons());
showAlert('Sucesso', 'Cronograma futuro cancelado.', 'success');
});
};
@ -430,14 +427,17 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
const handleUncancelAllFuture = () => {
showConfirm('Reativar Cronograma', 'Deseja realmente reativar TODAS as aulas futuras que estavam canceladas?', async () => {
const today = new Date().toISOString().split('T')[0];
const updatedLessons = (data.lessons || []).map(l => {
if (l.classId === classObj.id && l.status === 'cancelled' && l.date >= today) {
const updatedLessons = dbLessons.map(l => {
if (l.status === 'cancelled' && l.date >= today) {
return { ...l, status: 'scheduled', cancelReason: undefined };
}
return l;
});
updateData({ lessons: updatedLessons as Lesson[] });
await dbService.saveData({ ...data, lessons: updatedLessons as Lesson[] });
fetch('/api/aulas/lote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aulas: updatedLessons })
}).then(() => loadLessons());
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();
showAlert('Sucesso', 'Cronograma completo excluído.', 'success');
});

View File

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

View File

@ -29,7 +29,12 @@ const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
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 [deepLinkClassId, setDeepLinkClassId] = useState<string | null>(null);
// 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' }); }
});
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) => {
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) => {
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) => {
try { res.json({ contratos: await getContratos() }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
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) => {
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) => {
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 {
const { aulas } = req.body;
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 });
} catch (error) {
console.error('Erro ao inserir aulas em lote:', error);
@ -658,6 +708,14 @@ app.delete('/api/aulas/lote', async (req, res) => {
try {
const { ids } = req.body;
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 });
} catch (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) => {
try {
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 });
} catch (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) => {
try {
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 });
} catch (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) => {
try {
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 });
} catch (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();
let recordIndex = attendance.findIndex(a => a.studentId === req.user.studentId && a.date === fullDateStr);
if (recordIndex !== -1) {
const existing = attendance[recordIndex];
if (existing.type === 'presence') return res.status(400).json({ error: 'Não é possível justificar uma presença' });