From 1dc753c9c9a1e97d97b2508915acb69b76be1c54 Mon Sep 17 00:00:00 2001 From: Sidney Date: Tue, 26 May 2026 08:42:13 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Contratos=20100%=20SQL-First=20+=20cach?= =?UTF-8?q?e-buster=20em=20todos=20os=20m=C3=B3dulos=20+=20persist=C3=AAnc?= =?UTF-8?q?ia=20de=20aba=20ativa=20no=20F5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TABELA_DE_MIGRACAO_SQL.md | 15 ++-- manager/components/AttendanceQuery.tsx | 84 +++++++++++++++-------- manager/components/Contracts.tsx | 13 ++-- manager/components/Employees.tsx | 4 +- manager/components/Exams.tsx | 2 +- manager/components/Finance.tsx | 4 +- manager/components/LessonSchedule.tsx | 47 ++++++------- manager/components/Students.tsx | 2 +- manager/index.tsx | 7 +- manager/server.selfhosted.js | 94 ++++++++++++++++++++++++-- portal/server.selfhosted.js | 2 + 11 files changed, 191 insertions(+), 83 deletions(-) diff --git a/TABELA_DE_MIGRACAO_SQL.md b/TABELA_DE_MIGRACAO_SQL.md index be37881..54bc2d6 100644 --- a/TABELA_DE_MIGRACAO_SQL.md +++ b/TABELA_DE_MIGRACAO_SQL.md @@ -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. --- diff --git a/manager/components/AttendanceQuery.tsx b/manager/components/AttendanceQuery.tsx index 97333c5..62a477a 100644 --- a/manager/components/AttendanceQuery.tsx +++ b/manager/components/AttendanceQuery.tsx @@ -58,6 +58,38 @@ const AttendanceQuery: React.FC = ({ data, updateData, dee const [currentJustificationText, setCurrentJustificationText] = useState(''); const [currentRecordForJustification, setCurrentRecordForJustification] = useState(null); + const [dbAttendance, setDbAttendance] = useState(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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ data, updateData, dee
{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 = ({ 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 = ({ 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 = ({ data, updateData, dee {hasPendingJustification && (