refactor: Blindagem do Frontend contra JSON legado (Migrando Turmas, Cursos e Disciplinas para PostgreSQL nas abas restantes)

This commit is contained in:
Sidney 2026-05-24 18:26:24 -03:00
parent 2e0a041a26
commit b55633a191
10 changed files with 369 additions and 33 deletions

View File

@ -12,6 +12,41 @@ interface CertificatesProps {
const Certificates: React.FC<CertificatesProps> = ({ data, updateData }) => { const Certificates: React.FC<CertificatesProps> = ({ data, updateData }) => {
const { showAlert, showConfirm } = useDialog(); const { showAlert, showConfirm } = useDialog();
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
const [dbSubjects, setDbSubjects] = useState<any[]>(data?.subjects || []);
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/disciplinas').catch(() => ({ ok: false, json: async () => ({}) }))
]).then(async (responses) => {
const [resT, resC, resS] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
if (resS && resS.ok) {
const jsonS = await resS.json();
if (jsonS.disciplinas) setDbSubjects(jsonS.disciplinas.map((d: any) => ({
id: d.id, name: d.nome
})));
}
}).catch(console.error);
}, []);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedStudentId, setSelectedStudentId] = useState(''); const [selectedStudentId, setSelectedStudentId] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
@ -212,7 +247,7 @@ const Certificates: React.FC<CertificatesProps> = ({ data, updateData }) => {
const historico = (data.grades || []) const historico = (data.grades || [])
.filter(g => g.studentId === cert.studentId) .filter(g => g.studentId === cert.studentId)
.map(g => { .map(g => {
const subject = data.subjects.find(s => s.id === g.subjectId); const subject = dbSubjects.find(s => s.id === g.subjectId);
return `${subject?.name || 'Disciplina'}: ${g.value}`; return `${subject?.name || 'Disciplina'}: ${g.value}`;
}) })
.join('\n'); .join('\n');
@ -266,7 +301,7 @@ const Certificates: React.FC<CertificatesProps> = ({ data, updateData }) => {
const currentHistorico = (data.grades || []) const currentHistorico = (data.grades || [])
.filter(g => g.studentId === selectedStudentId) .filter(g => g.studentId === selectedStudentId)
.map(g => { .map(g => {
const subject = data.subjects.find(s => s.id === g.subjectId); const subject = dbSubjects.find(s => s.id === g.subjectId);
return `${subject?.name || 'Disciplina'}: ${g.value}`; return `${subject?.name || 'Disciplina'}: ${g.value}`;
}) })
.join('\n') || 'Matemática: 9.5\nPortuguês: 8.0\nHistória: 10.0'; .join('\n') || 'Matemática: 9.5\nPortuguês: 8.0\nHistória: 10.0';

View File

@ -11,6 +11,34 @@ interface ContractsProps {
const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => { const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
const { showAlert, showConfirm } = useDialog(); const { showAlert, showConfirm } = useDialog();
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
]).then(async (responses) => {
const [resT, resC] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
}).catch(console.error);
}, []);
const [activeTab, setActiveTab] = useState<'contracts' | 'templates'>('contracts'); const [activeTab, setActiveTab] = useState<'contracts' | 'templates'>('contracts');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false); const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false);
@ -42,8 +70,8 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
useEffect(() => { useEffect(() => {
if (formData.studentId && !formData.content) { if (formData.studentId && !formData.content) {
const student = data.students.find(s => s.id === formData.studentId); const student = data.students.find(s => s.id === formData.studentId);
const cls = data.classes.find(c => c.id === student?.classId); const cls = dbClasses.find(c => c.id === student?.classId);
const course = data.courses.find(c => c.id === cls?.courseId); const course = dbCourses.find(c => c.id === cls?.courseId);
const templateObj = data.contractTemplates?.find(t => t.id === student?.contractTemplateId); const templateObj = data.contractTemplates?.find(t => t.id === student?.contractTemplateId);
if (student && course) { if (student && course) {
@ -190,8 +218,8 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
const openGenerateModal = (contract: Contract) => { const openGenerateModal = (contract: Contract) => {
const student = data.students.find(s => s.id === contract.studentId); const student = data.students.find(s => s.id === contract.studentId);
const cls = data.classes.find(c => c.id === student?.classId); const cls = dbClasses.find(c => c.id === student?.classId);
const course = data.courses.find(c => c.id === cls?.courseId); const course = dbCourses.find(c => c.id === cls?.courseId);
if (student && cls && course) { if (student && cls && course) {
setContractToGenerate(contract); setContractToGenerate(contract);
@ -211,8 +239,8 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
const contract = contractToGenerate; const contract = contractToGenerate;
const student = data.students.find(s => s.id === contract.studentId); const student = data.students.find(s => s.id === contract.studentId);
const cls = data.classes.find(c => c.id === student?.classId); const cls = dbClasses.find(c => c.id === student?.classId);
const course = data.courses.find(c => c.id === cls?.courseId); const course = dbCourses.find(c => c.id === cls?.courseId);
if (!course || !student) return; if (!course || !student) return;

View File

@ -41,10 +41,27 @@ interface DashboardProps {
const Dashboard: React.FC<DashboardProps> = ({ data }) => { const Dashboard: React.FC<DashboardProps> = ({ data }) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const [dashboardView, setDashboardView] = useState<'standard' | 'detailed'>('standard'); const [dashboardView, setDashboardView] = useState<'standard' | 'detailed'>('standard');
const [dbClasses, setDbClasses] = useState<any[]>(data.classes || []);
React.useEffect(() => {
fetch('/api/turmas')
.then(res => res.json())
.then(json => {
if (json.turmas) {
setDbClasses(json.turmas.map((t: any) => ({
id: t.id,
name: t.nome,
courseId: t.curso_id,
maxStudents: Number(t.max_alunos || 0)
})));
}
})
.catch(console.error);
}, []);
// Basic Stats // Basic Stats
const activeStudents = useMemo(() => data.students.filter(s => s.status === 'active').length, [data.students]); const activeStudents = useMemo(() => data.students.filter(s => s.status === 'active').length, [data.students]);
const totalClasses = useMemo(() => data.classes.length, [data.classes]); const totalClasses = useMemo(() => dbClasses.length, [dbClasses]);
const pendingPayments = useMemo(() => data.payments.filter(p => p.status === 'pending').length, [data.payments]); const pendingPayments = useMemo(() => data.payments.filter(p => p.status === 'pending').length, [data.payments]);
const revenue = useMemo(() => data.payments const revenue = useMemo(() => data.payments
.filter(p => p.status === 'paid') .filter(p => p.status === 'paid')
@ -76,11 +93,11 @@ const Dashboard: React.FC<DashboardProps> = ({ data }) => {
}, [data.lessons]); }, [data.lessons]);
// Chart Data: Class Occupancy // Chart Data: Class Occupancy
const classOccupancy = useMemo(() => data.classes.map(c => ({ const classOccupancy = useMemo(() => dbClasses.map(c => ({
name: c.name, name: c.name,
students: data.students.filter(s => s.classId === c.id).length, students: data.students.filter(s => s.classId === c.id).length,
capacity: 20 // Assuming a default capacity capacity: c.maxStudents || 20 // Usando a capacidade real se disponível
})).sort((a, b) => b.students - a.students), [data.classes, data.students]); })).sort((a, b) => b.students - a.students), [dbClasses, data.students]);
// Chart Data: Payment Status // Chart Data: Payment Status
const paymentStatus = useMemo(() => [ const paymentStatus = useMemo(() => [

View File

@ -20,6 +20,41 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos'); const [activeTab, setActiveTab] = useState<'ativos' | 'lixeira'>('ativos');
const { showAlert, showConfirm } = useDialog(); const { showAlert, showConfirm } = useDialog();
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
const [dbSubjects, setDbSubjects] = useState<any[]>(data?.subjects || []);
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/disciplinas').catch(() => ({ ok: false, json: async () => ({}) }))
]).then(async (responses) => {
const [resT, resC, resS] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
if (resS && resS.ok) {
const jsonS = await resS.json();
if (jsonS.disciplinas) setDbSubjects(jsonS.disciplinas.map((d: any) => ({
id: d.id, name: d.nome
})));
}
}).catch(console.error);
}, []);
const normalizePhotoUrl = (url?: string) => { const normalizePhotoUrl = (url?: string) => {
if (!url) return ''; if (!url) return '';
if (url.startsWith('data:image') || url.startsWith('/storage')) return url; if (url.startsWith('data:image') || url.startsWith('/storage')) return url;
@ -35,14 +70,14 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
const filteredExams = exams.filter(exam => const filteredExams = exams.filter(exam =>
(activeTab === 'ativos' ? !exam.isDeleted : !!exam.isDeleted) && (activeTab === 'ativos' ? !exam.isDeleted : !!exam.isDeleted) &&
(exam.title.toLowerCase().includes(searchTerm.toLowerCase()) || (exam.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
data.classes.find(c => c.id === exam.classId)?.name.toLowerCase().includes(searchTerm.toLowerCase())) dbClasses.find(c => c.id === exam.classId)?.name.toLowerCase().includes(searchTerm.toLowerCase()))
); );
const handleStartCreate = () => { const handleStartCreate = () => {
setEditingExam({ setEditingExam({
id: Date.now().toString(), id: Date.now().toString(),
title: '', title: '',
classId: data.classes[0]?.id || '', classId: dbClasses[0]?.id || '',
durationMinutes: 60, durationMinutes: 60,
status: 'draft', status: 'draft',
questions: [], questions: [],
@ -194,7 +229,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
}; };
const handleNotifyStudents = async (exam: Exam) => { const handleNotifyStudents = async (exam: Exam) => {
const classObj = (data.classes || []).find(c => c.id === exam.classId); const classObj = (dbClasses || []).find(c => c.id === exam.classId);
if (!classObj) return; if (!classObj) return;
showConfirm( showConfirm(
@ -318,7 +353,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800"
> >
<option value="" disabled>Selecione uma turma</option> <option value="" disabled>Selecione uma turma</option>
{data.classes.map(c => ( {dbClasses.map(c => (
<option key={c.id} value={c.id}>{c.name}</option> <option key={c.id} value={c.id}>{c.name}</option>
))} ))}
</select> </select>
@ -343,7 +378,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800"
> >
<option value="">Nenhuma (não vincular)</option> <option value="">Nenhuma (não vincular)</option>
{(data.subjects || []).map(s => ( {(dbSubjects || []).map(s => (
<option key={s.id} value={s.id}>{s.name}</option> <option key={s.id} value={s.id}>{s.name}</option>
))} ))}
</select> </select>
@ -575,7 +610,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
) : ( ) : (
<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">
{filteredExams.map(exam => { {filteredExams.map(exam => {
const classObj = data.classes.find(c => c.id === exam.classId); const classObj = dbClasses.find(c => c.id === exam.classId);
return ( return (
<div key={exam.id} className={`rounded-2xl p-6 shadow-sm border transition-shadow relative overflow-hidden group ${exam.isDeleted ? 'bg-slate-50 border-slate-200 opacity-80' : 'bg-white border-slate-100 hover:shadow-md'}`}> <div key={exam.id} className={`rounded-2xl p-6 shadow-sm border transition-shadow relative overflow-hidden group ${exam.isDeleted ? 'bg-slate-50 border-slate-200 opacity-80' : 'bg-white border-slate-100 hover:shadow-md'}`}>
<div className={`absolute top-0 left-0 w-1.5 h-full rounded-l-2xl ${exam.isDeleted ? 'bg-slate-400' : 'bg-indigo-500'}`}></div> <div className={`absolute top-0 left-0 w-1.5 h-full rounded-l-2xl ${exam.isDeleted ? 'bg-slate-400' : 'bg-indigo-500'}`}></div>
@ -613,7 +648,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
{exam.subjectId && ( {exam.subjectId && (
<p className="text-sm text-slate-500 flex items-center gap-2"> <p className="text-sm text-slate-500 flex items-center gap-2">
<span className="font-bold text-slate-700">Disciplina:</span> <span className="font-bold text-slate-700">Disciplina:</span>
{(data.subjects || []).find(s => s.id === exam.subjectId)?.name || '—'} {(dbSubjects || []).find(s => s.id === exam.subjectId)?.name || '—'}
</p> </p>
)} )}
{exam.periodId && ( {exam.periodId && (
@ -716,7 +751,7 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
onChange={e => setTargetClassId(e.target.value)} onChange={e => setTargetClassId(e.target.value)}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700"
> >
{data.classes.map(c => ( {dbClasses.map(c => (
<option key={c.id} value={c.id}>{c.name}</option> <option key={c.id} value={c.id}>{c.name}</option>
))} ))}
</select> </select>

View File

@ -13,6 +13,34 @@ interface LessonScheduleProps {
const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateData, onClose }) => { const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateData, onClose }) => {
const { showAlert, showConfirm } = useDialog(); const { showAlert, showConfirm } = useDialog();
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
]).then(async (responses) => {
const [resT, resC] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
}).catch(console.error);
}, []);
const [showGenerateModal, setShowGenerateModal] = useState(false); const [showGenerateModal, setShowGenerateModal] = useState(false);
const [showLessonDetail, setShowLessonDetail] = useState<Lesson | null>(null); const [showLessonDetail, setShowLessonDetail] = useState<Lesson | null>(null);
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
@ -53,7 +81,7 @@ const LessonSchedule: React.FC<LessonScheduleProps> = ({ classObj, data, updateD
// Só dá conflito se for na mesma TURMA ou com o mesmo PROFESSOR // Só dá conflito se for na mesma TURMA ou com o mesmo PROFESSOR
const isSameClass = l.classId === classObj.id; const isSameClass = l.classId === classObj.id;
const otherClass = data.classes.find(c => c.id === l.classId); const otherClass = dbClasses.find(c => c.id === l.classId);
const isSameTeacher = otherClass && classObj.teacher && otherClass.teacher === classObj.teacher; const isSameTeacher = otherClass && classObj.teacher && otherClass.teacher === classObj.teacher;
if (!isSameClass && !isSameTeacher) return false; if (!isSameClass && !isSameTeacher) return false;

View File

@ -28,6 +28,34 @@ const defaultTemplates = {
const Messages: React.FC<MessagesProps> = ({ data, updateData }) => { const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
const { showAlert, showConfirm } = useDialog(); const { showAlert, showConfirm } = useDialog();
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
]).then(async (responses) => {
const [resT, resC] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
}).catch(console.error);
}, []);
const defaultVars = data.messageTemplates || defaultTemplates; const defaultVars = data.messageTemplates || defaultTemplates;
const initRules = defaultVars.automationRules || defaultTemplates.automationRules; const initRules = defaultVars.automationRules || defaultTemplates.automationRules;
@ -356,7 +384,7 @@ const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
> >
<option value="">-- Selecione --</option> <option value="">-- Selecione --</option>
{targetType === 'turma' {targetType === 'turma'
? data.classes?.map(c => <option key={c.id} value={c.id}>{c.name}</option>) ? dbClasses?.map(c => <option key={c.id} value={c.id}>{c.name}</option>)
: data.students?.map(s => <option key={s.id} value={s.id}>{s.name}</option>) : data.students?.map(s => <option key={s.id} value={s.id}>{s.name}</option>)
} }
</select> </select>

View File

@ -41,6 +41,33 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
const periods = data.periods || []; const periods = data.periods || [];
const grades = data.grades || []; const grades = data.grades || [];
const [dbClasses, setDbClasses] = useState<Class[]>(data.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data.courses || []);
const loadClassesAndCourses = async () => {
try {
const [clsRes, crsRes] = await Promise.all([
fetch('/api/turmas'),
fetch('/api/cursos')
]);
const clsData = await clsRes.json();
const crsData = await crsRes.json();
if (clsData.turmas) {
setDbClasses(clsData.turmas.map((t: any) => ({
id: t.id,
name: t.nome,
courseId: t.curso_id
})));
}
if (crsData.cursos) {
setDbCourses(crsData.cursos);
}
} catch(e) {
console.error('Erro ao buscar turmas/cursos:', e);
}
};
const loadSubjects = async () => { const loadSubjects = async () => {
try { try {
const res = await fetch('/api/disciplinas'); const res = await fetch('/api/disciplinas');
@ -59,6 +86,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
React.useEffect(() => { React.useEffect(() => {
loadSubjects(); loadSubjects();
loadClassesAndCourses();
}, []); }, []);
// Buscar todas as notas da turma para mostrar médias na lista // Buscar todas as notas da turma para mostrar médias na lista
@ -361,7 +389,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
return (totalSum / subjectAverages.length).toFixed(2); return (totalSum / subjectAverages.length).toFixed(2);
}; };
const filteredClasses = data.classes.filter(c => const filteredClasses = dbClasses.filter(c =>
(c.name || '').toLowerCase().includes((searchTerm || '').toLowerCase()) (c.name || '').toLowerCase().includes((searchTerm || '').toLowerCase())
); );
@ -494,7 +522,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
<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">
{filteredClasses.map(cls => { {filteredClasses.map(cls => {
const course = data.courses.find(c => c.id === cls.courseId); const course = dbCourses.find(c => c.id === cls.courseId);
const studentCount = data.students.filter(s => s.classId === cls.id).length; const studentCount = data.students.filter(s => s.classId === cls.id).length;
return ( return (
<div <div
@ -511,7 +539,7 @@ const ReportCard: React.FC<ReportCardProps> = ({ data, updateData }) => {
</div> </div>
<div> <div>
<h3 className="font-black text-slate-800 text-lg">{cls.name}</h3> <h3 className="font-black text-slate-800 text-lg">{cls.name}</h3>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">{course?.name || 'Curso não encontrado'}</p> <p className="text-xs font-bold text-slate-400 uppercase tracking-widest">{course?.nome || course?.name || 'Curso não encontrado'}</p>
</div> </div>
</div> </div>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">

View File

@ -39,6 +39,27 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
const [cancellationReason, setCancellationReason] = useState(''); const [cancellationReason, setCancellationReason] = useState('');
const [selectedClassId, setSelectedClassId] = useState<string | null>(null); const [selectedClassId, setSelectedClassId] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [dbClasses, setDbClasses] = useState<any[]>(dbClasses || []);
const [dbCourses, setDbCourses] = useState<any[]>(dbCourses || []);
useEffect(() => {
Promise.all([
fetch('/api/turmas'),
fetch('/api/cursos')
]).then(async ([resT, resC]) => {
if(resT.ok && resC.ok) {
const jsonT = await resT.json();
const jsonC = await resC.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome
})));
}
}).catch(console.error);
}, []);
// Academic History State // Academic History State
const [historyGrades, setHistoryGrades] = useState<any[]>([]); const [historyGrades, setHistoryGrades] = useState<any[]>([]);
@ -818,8 +839,8 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
} }
// Process Generate Fee and Contract // Process Generate Fee and Contract
const studentClass = data.classes.find(c => c.id === formData.classId); const studentClass = dbClasses.find(c => c.id === formData.classId);
const course = studentClass ? data.courses.find(c => c.id === studentClass.courseId) : null; const course = studentClass ? dbCourses.find(c => c.id === studentClass.courseId) : null;
if ((formData as any).generateFee && course) { if ((formData as any).generateFee && course) {
const feeAmount = (course.registrationFee || 0) - (formData.discount || 0); const feeAmount = (course.registrationFee || 0) - (formData.discount || 0);
@ -1173,9 +1194,9 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
{(activeTab === 'active' && !selectedClassId && !searchTerm) ? ( {(activeTab === 'active' && !selectedClassId && !searchTerm) ? (
<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 => { {dbClasses.map(cls => {
const studentCount = data.students.filter(s => s.classId === cls.id && (activeTab === 'active' ? s.status !== 'cancelled' : s.status === 'cancelled')).length; const studentCount = data.students.filter(s => s.classId === cls.id && (activeTab === 'active' ? s.status !== 'cancelled' : s.status === 'cancelled')).length;
const course = data.courses.find(c => c.id === cls.courseId); const course = dbCourses.find(c => c.id === cls.courseId);
return ( return (
<button <button
@ -1242,7 +1263,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
</button> </button>
{selectedClassId && ( {selectedClassId && (
<h4 className="text-lg font-black text-slate-800"> <h4 className="text-lg font-black text-slate-800">
{selectedClassId === 'none' ? 'Alunos Sem Turma' : data.classes.find(c => c.id === selectedClassId)?.name} {selectedClassId === 'none' ? 'Alunos Sem Turma' : dbClasses.find(c => c.id === selectedClassId)?.name}
</h4> </h4>
)} )}
</div> </div>
@ -1261,7 +1282,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
</thead> </thead>
<tbody className="text-sm divide-y divide-slate-50"> <tbody className="text-sm divide-y divide-slate-50">
{filteredStudents.map(student => { {filteredStudents.map(student => {
const studentClass = data.classes.find(c => c.id === student.classId); const studentClass = dbClasses.find(c => c.id === student.classId);
return ( return (
<tr key={student.id} className={`hover:bg-slate-50 transition-colors group ${student.status === 'cancelled' ? 'bg-slate-50 opacity-60 grayscale' : ''}`}> <tr key={student.id} className={`hover:bg-slate-50 transition-colors group ${student.status === 'cancelled' ? 'bg-slate-50 opacity-60 grayscale' : ''}`}>
<td className="p-4"> <td className="p-4">
@ -1752,7 +1773,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
onChange={e => setFormData({...formData, classId: e.target.value})} onChange={e => setFormData({...formData, classId: e.target.value})}
> >
<option value="">Selecione uma turma...</option> <option value="">Selecione uma turma...</option>
{data.classes.map(c => ( {dbClasses.map(c => (
<option key={c.id} value={c.id}>{c.name} - {c.schedule}</option> <option key={c.id} value={c.id}>{c.name} - {c.schedule}</option>
))} ))}
</select> </select>
@ -1873,7 +1894,7 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
onChange={e => setNewClassId(e.target.value)} onChange={e => setNewClassId(e.target.value)}
> >
<option value="">Selecione uma turma...</option> <option value="">Selecione uma turma...</option>
{data.classes.filter(c => c.id !== transferringStudent.classId).map(c => ( {dbClasses.filter(c => c.id !== transferringStudent.classId).map(c => (
<option key={c.id} value={c.id}>{c.name} - {c.schedule}</option> <option key={c.id} value={c.id}>{c.name} - {c.schedule}</option>
))} ))}
</select> </select>

View File

@ -0,0 +1,74 @@
const fs = require('fs');
const path = require('path');
function processFile(filename, replaceSubjects = false) {
const filePath = path.join(__dirname, '../components', filename);
if (!fs.existsSync(filePath)) return;
let content = fs.readFileSync(filePath, 'utf8');
// Inject states and fetch
// Find a good place to inject. Usually after `const [isModalOpen, setIsModalOpen] = useState(false);` or similar.
// We'll just look for a known pattern or add it at the top of the component.
// We will find `const { showAlert, showConfirm } = useDialog();` and inject right after it
const hookDecl = `const { showAlert, showConfirm } = useDialog();`;
let fetchBlock = `
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
${replaceSubjects ? 'const [dbSubjects, setDbSubjects] = useState<any[]>(data?.subjects || []);' : ''}
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
${replaceSubjects ? "fetch('/api/disciplinas').catch(() => ({ ok: false, json: async () => ({}) }))" : ''}
]).then(async (responses) => {
const [resT, resC${replaceSubjects ? ', resS' : ''}] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
${replaceSubjects ? `
if (resS && resS.ok) {
const jsonS = await resS.json();
if (jsonS.disciplinas) setDbSubjects(jsonS.disciplinas.map((d: any) => ({
id: d.id, name: d.nome
})));
}
` : ''}
}).catch(console.error);
}, []);
`;
if (content.includes(hookDecl) && !content.includes('const [dbClasses')) {
content = content.replace(hookDecl, hookDecl + '\n' + fetchBlock);
// Replace usages
content = content.replace(/data\.classes/g, 'dbClasses');
content = content.replace(/data\.courses/g, 'dbCourses');
if (replaceSubjects) {
content = content.replace(/data\.subjects/g, 'dbSubjects');
}
fs.writeFileSync(filePath, content, 'utf8');
console.log(filename + ' atualizado.');
}
}
processFile('Finance.tsx', false);
processFile('Exams.tsx', true);
processFile('Contracts.tsx', false);
processFile('LessonSchedule.tsx', false);
processFile('AttendanceQuery.tsx', false);
processFile('Certificates.tsx', true);
processFile('Messages.tsx', false);

View File

@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, '../components/Students.tsx');
let content = fs.readFileSync(filePath, 'utf8');
// Adicionar os states
const stateCode = ` const [isSaving, setIsSaving] = useState(false);
const [dbClasses, setDbClasses] = useState<any[]>(data.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data.courses || []);
useEffect(() => {
Promise.all([
fetch('/api/turmas'),
fetch('/api/cursos')
]).then(async ([resT, resC]) => {
if(resT.ok && resC.ok) {
const jsonT = await resT.json();
const jsonC = await resC.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome
})));
}
}).catch(console.error);
}, []);`;
content = content.replace(' const [isSaving, setIsSaving] = useState(false);', stateCode);
// Substituições seguras
content = content.replace(/data\.classes/g, 'dbClasses');
content = content.replace(/data\.courses/g, 'dbCourses');
// E arrumar uma possível falha caso haja "dbClasses" onde deveria ser data.classes no destructuring ou useEffect dependencias
// Por exemplo: se ele usava deepLinkClassId dependendo de data.classes
content = content.replace(/dbClasses, dbCourses, data\.students/g, 'dbClasses, dbCourses, data.students');
fs.writeFileSync(filePath, content, 'utf8');
console.log('Students.tsx atualizado com sucesso.');