import { jsPDF } from 'jspdf'; import 'jspdf-autotable'; import { SchoolData, Student, Contract, Payment, Class } from '../types'; import { getImageDimensions } from './imageService'; /** * Helper to calculate proportional dimensions and add image to PDF */ const addImageProportional = async (doc: any, src: string, x: number, y: number, maxW: number, maxH: number) => { try { const { width, height } = await getImageDimensions(src); const ratio = width / height; let finalW = maxW; let finalH = maxW / ratio; if (finalH > maxH) { finalH = maxH; finalW = maxH * ratio; } // Center in the box const offsetX = (maxW - finalW) / 2; const offsetY = (maxH - finalH) / 2; let format = 'JPEG'; const lowerSrc = src.toLowerCase(); if (lowerSrc.includes('png')) format = 'PNG'; else if (lowerSrc.includes('webp')) format = 'WEBP'; doc.addImage(src, format, x + offsetX, y + offsetY, finalW, finalH, undefined, 'FAST'); return { width: finalW, height: finalH }; } catch (e) { console.warn("Image failed to load in PDF", e); return null; } }; /** * Helper to process and add a 3x4 student photo with center crop and compression */ const addStudentPhoto3x4 = async (doc: any, src: string, x: number, y: number) => { return new Promise<{ width: number, height: number } | null>((resolve) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { const canvas = document.createElement('canvas'); // 300x400 for 300 DPI approx (30mm x 40mm) const targetW = 354; // 30mm at 300 DPI is approx 354px const targetH = 472; // 40mm at 300 DPI is approx 472px canvas.width = targetW; canvas.height = targetH; const ctx = canvas.getContext('2d'); if (!ctx) { resolve(null); return; } const imgRatio = img.width / img.height; const targetRatio = targetW / targetH; let sourceW, sourceH, sourceX, sourceY; if (imgRatio > targetRatio) { // Image is wider than 3x4 - crop sides sourceH = img.height; sourceW = img.height * targetRatio; sourceX = (img.width - sourceW) / 2; sourceY = 0; } else { // Image is taller than 3x4 - crop top/bottom sourceW = img.width; sourceH = img.width / targetRatio; sourceX = 0; sourceY = (img.height - sourceH) / 2; } ctx.drawImage(img, sourceX, sourceY, sourceW, sourceH, 0, 0, targetW, targetH); // Compress to JPEG with 0.6 quality for instant opening const dataUrl = canvas.toDataURL('image/jpeg', 0.6); doc.addImage(dataUrl, 'JPEG', x, y, 30, 40, undefined, 'FAST'); resolve({ width: 30, height: 40 }); }; img.onerror = () => resolve(null); img.src = src; }); }; /** * Helper to add header/logo to PDF */ export const addHeader = async (doc: any, schoolData: SchoolData) => { const profile = schoolData.profile; if (schoolData.logo) { await addImageProportional(doc, schoolData.logo, 20, 10, 25, 25); } doc.setFontSize(12); doc.setTextColor(0); doc.setFont('helvetica', 'bold'); doc.text(profile.name || 'EduManager School', 50, 18); doc.setFontSize(8); doc.setTextColor(0); doc.setFont('helvetica', 'normal'); doc.text(`CNPJ: ${profile.cnpj || 'Não informado'}`, 50, 23); doc.text(profile.address || '', 50, 27); doc.text(`${profile.phone || ''} ${profile.email ? '| ' + profile.email : ''}`, 50, 31); doc.setDrawColor(0); doc.setLineWidth(0.1); doc.line(20, 38, 190, 38); return 45; }; /** * Helper to calculate age and get signer info */ const getSignerInfo = (student: Student) => { if (!student.birthDate) { return { name: student.guardianName || student.name, cpf: student.guardianCpf || student.cpf, label: student.guardianName ? 'ASSINATURA DO RESPONSÁVEL' : 'ASSINATURA DO ALUNO' }; } const today = new Date(); const birth = new Date(student.birthDate); let age = today.getFullYear() - birth.getFullYear(); const m = today.getMonth() - birth.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) { age--; } if (age >= 18) { return { name: student.name, cpf: student.cpf, label: 'ASSINATURA DO ALUNO' }; } else { return { name: student.guardianName || 'NÃO INFORMADO', cpf: student.guardianCpf || '---', label: 'ASSINATURA DO RESPONSÁVEL' }; } }; /** * Helper to get Director info from employees */ const getDirectorInfo = (schoolData: SchoolData) => { const employees = schoolData.employees || []; const categories = schoolData.employeeCategories || []; const director = employees.find(e => { const cat = categories.find(c => c.id === e.categoryId); const catName = cat?.name.toLowerCase() || ''; const empName = e.name.toLowerCase(); const roleMatch = catName.includes('diretor') || catName.includes('diretoria'); const nameMatch = empName.includes('diretor') || empName.includes('diretoria'); return roleMatch || nameMatch; }); if (director) { return { name: director.name, cpf: director.cpf, role: 'Diretor' }; } return null; }; /** * Helper to add page numbers to footer */ const addPageNumbers = (doc: any) => { const pageCount = doc.internal.getNumberOfPages(); doc.setFont('helvetica', 'normal'); doc.setFontSize(7); doc.setTextColor(0); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.text( `Página ${i} de ${pageCount}`, doc.internal.pageSize.width / 2, doc.internal.pageSize.height - 10, { align: 'center' } ); } }; /** * Helper to draw justified text with paragraph support */ const drawJustifiedText = async (doc: any, text: string, x: number, y: number, maxWidth: number, lineHeight: number, schoolData: SchoolData) => { const paragraphs = text.split('\n').filter(p => p.trim() !== ''); let currentY = y; const margin = x; const pageHeight = doc.internal.pageSize.height; for (const p of paragraphs) { const isClause = p.toUpperCase().startsWith('CLÁUSULA') || p.toUpperCase().startsWith('CLAUSULA'); // Check for page break before paragraph if (currentY > pageHeight - 30) { doc.addPage(); await addHeader(doc, schoolData); currentY = 50; } doc.setFont('helvetica', isClause ? 'bold' : 'normal'); doc.setFontSize(9); // Indent for non-clause paragraphs const startX = isClause ? margin : margin + 10; const currentMaxWidth = isClause ? maxWidth : maxWidth - 10; const lines = doc.splitTextToSize(p, currentMaxWidth); for (let i = 0; i < lines.length; i++) { // Check for page break before line if (currentY > pageHeight - 20) { doc.addPage(); await addHeader(doc, schoolData); currentY = 50; doc.setFont('helvetica', isClause ? 'bold' : 'normal'); } const line = lines[i]; const isLastLine = i === lines.length - 1; if (isClause || isLastLine || line.trim().length < (currentMaxWidth / 4)) { doc.text(line, startX, currentY); } else { // Justify line const words = line.trim().split(/\s+/); if (words.length > 1) { const totalWordsWidth = words.reduce((sum: number, word: string) => sum + doc.getTextWidth(word), 0); const totalSpacing = currentMaxWidth - totalWordsWidth; const spacingPerWord = totalSpacing / (words.length - 1); let currentX = startX; for (let j = 0; j < words.length; j++) { doc.text(words[j], currentX, currentY); currentX += doc.getTextWidth(words[j]) + spacingPerWord; } } else { doc.text(line, startX, currentY); } } currentY += lineHeight; } currentY += 4; // Space between paragraphs } return currentY; }; /** * Helper to draw justified text specifically for the contract */ const drawContractText = async (doc: any, text: string, x: number, y: number, maxWidth: number, lineHeight: number, schoolData: SchoolData) => { let cleanText = text .replace(/PROFISSINALIZANTE/g, 'PROFISSIONALIZANTE') .replace(/CONTRADA/g, 'CONTRATADA') .replace(/terar/g, 'terá') .replace(/apredisagem/g, 'aprendizagem'); // Preserve the exact organization by splitting strictly by newline let paragraphs = cleanText.split('\n'); let currentY = y; const margin = x; const pageHeight = doc.internal.pageSize.height; const bottomMargin = 20; const fontSize = 11; // Uniform font size for the body for (const p of paragraphs) { if (!p.trim()) { currentY += lineHeight * 0.5; // Provide spacing for empty lines continue; } const isClause = /^(CLÁUSULA|CLAUSULA)/i.test(p.trim()); if (currentY > pageHeight - bottomMargin - 10) { doc.addPage(); currentY = await addHeader(doc, schoolData); } doc.setFont('helvetica', 'normal'); doc.setFontSize(fontSize); let title = ""; let restOfText = p.trim(); if (isClause) { const match = restOfText.match(/^(CLÁUSULA\s+\d+.*?[-–—:]\s*|CLAUSULA\s+\d+.*?[-–—:]\s*)/i); if (match) { title = match[0]; restOfText = restOfText.substring(title.length).trim(); } else { const match2 = restOfText.match(/^(CLÁUSULA\s+\d+|CLAUSULA\s+\d+)/i); if (match2) { title = match2[0] + " - "; restOfText = restOfText.substring(match2[0].length).trim(); } } } const startX = margin; const currentMaxWidth = maxWidth; if (title) { if (currentY > pageHeight - bottomMargin - 10) { doc.addPage(); currentY = await addHeader(doc, schoolData); } doc.setFont('helvetica', 'bold'); doc.text(title, startX, currentY); currentY += lineHeight; doc.setFont('helvetica', 'normal'); } if (!restOfText) { currentY += lineHeight * 0.5; continue; } const lines = doc.splitTextToSize(restOfText, currentMaxWidth); for (let i = 0; i < lines.length; i++) { if (currentY > pageHeight - bottomMargin) { doc.addPage(); currentY = await addHeader(doc, schoolData); doc.setFont('helvetica', 'normal'); doc.setFontSize(fontSize); } const line = lines[i]; const isLastLine = i === lines.length - 1; if (isLastLine || line.trim().length < (currentMaxWidth / 2)) { doc.text(line, startX, currentY); } else { // Justify line const words = line.trim().split(/\s+/); if (words.length > 1) { const totalWordsWidth = words.reduce((sum: number, word: string) => sum + doc.getTextWidth(word), 0); const totalSpacing = currentMaxWidth - totalWordsWidth; const spacingPerWord = totalSpacing / (words.length - 1); let currentX = startX; for (let j = 0; j < words.length; j++) { doc.text(words[j], currentX, currentY); currentX += doc.getTextWidth(words[j]) + spacingPerWord; } } else { doc.text(line, startX, currentY); } } currentY += lineHeight; } currentY += lineHeight * 0.5; // Space between paragraphs } return currentY; }; export const pdfService = { generateStudentRegistrationPDF: async (student: Student, schoolData: SchoolData) => { const doc = new jsPDF() as any; const startY = await addHeader(doc, schoolData); const cls = schoolData.classes.find(c => c.id === student.classId); const course = schoolData.courses.find(c => c.id === cls?.courseId); // Title and Date (Centered) doc.setFontSize(16); doc.setTextColor(0); doc.setFont('helvetica', 'bold'); doc.text('Ficha de Matrícula', 105, startY + 10, { align: 'center' }); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text(`Data: ${new Date().toLocaleDateString('pt-BR')}`, 105, startY + 16, { align: 'center' }); // Photo Positioning - Top Right (Standard 3x4cm = 30x40mm) const photoX = 155; const photoY = startY + 25; const photoW = 30; const photoH = 40; if (student.photo) { await addStudentPhoto3x4(doc, student.photo, photoX, photoY); } // Border around photo area doc.setDrawColor(0); doc.setLineWidth(0.1); doc.rect(photoX, photoY, photoW, photoH); if (!student.photo) { doc.setFontSize(7); doc.setTextColor(150); doc.text('FOTO 3X4', photoX + 15, photoY + 20, { align: 'center' }); } let currentY = startY + 30; const labelX = 20; // 1. Dados do Aluno doc.setFontSize(12); doc.setTextColor(0); doc.setFont('helvetica', 'bold'); doc.text('Dados do Aluno', labelX, currentY); currentY += 8; doc.setFontSize(10); const drawField = (label: string, value: string, y: number) => { doc.setFont('helvetica', 'normal'); doc.text(`${label}: ${value || '-'}`, labelX, y); return y + 6; }; currentY = drawField('Nº Matrícula', student.enrollmentNumber || 'Não gerado', currentY); currentY = drawField('Nome', student.name, currentY); currentY = drawField('CPF', student.cpf, currentY); currentY = drawField('RG', student.rg, currentY); currentY = drawField('Data de Nascimento', student.birthDate ? student.birthDate.split('-').reverse().join('/') : '', currentY); currentY = drawField('Email', student.email, currentY); currentY = drawField('Telefone', student.phone, currentY); currentY += 4; // 2. Endereço doc.setFontSize(11); doc.setFont('helvetica', 'bold'); doc.text('Endereço', labelX, currentY); currentY += 6; doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text(`${student.addressStreet || ''}${student.addressNumber ? `, ${student.addressNumber}` : ''} - ${student.addressNeighborhood || ''}`, labelX, currentY); currentY += 6; doc.text(`${student.addressCity || ''} - ${student.addressState || ''} CEP: ${student.addressZip || ''}`, labelX, currentY); currentY += 10; // 3. Dados do Curso doc.setFontSize(11); doc.setFont('helvetica', 'bold'); doc.text('Dados do Curso', labelX, currentY); currentY += 8; doc.setFontSize(10); currentY = drawField('Curso', course?.name || 'Não vinculado', currentY); currentY = drawField('Turma', cls?.name || 'Não atribuída', currentY); currentY = drawField('Horário', cls?.schedule || 'N/A', currentY); currentY = drawField('Professor', cls?.teacher || 'N/A', currentY); currentY = drawField('Data Matrícula', student.registrationDate ? student.registrationDate.split('T')[0].split('-').reverse().join('/') : '', currentY); currentY += 10; // 4. Termos e Condições doc.setFontSize(11); doc.setFont('helvetica', 'bold'); doc.text('Termos e Condições', labelX, currentY); currentY += 8; doc.setFontSize(9); doc.setFont('helvetica', 'normal'); const termsText = "Declaro que as informações acima são verdadeiras e assumo a responsabilidade pelo pagamento das mensalidades escolares conforme contrato de prestação de serviços educacionais."; const splitTerms = doc.splitTextToSize(termsText, 170); doc.text(splitTerms, labelX, currentY); // Footer - Signatures const pageHeight = doc.internal.pageSize.height; const signer = getSignerInfo(student); doc.setDrawColor(0); doc.setLineWidth(0.2); // Signer Signature const sigY = pageHeight - 45; doc.line(20, sigY, 90, sigY); doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.text(signer.name.toUpperCase(), 55, sigY + 5, { align: 'center' }); doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.text(`CPF: ${signer.cpf || '---'}`, 55, sigY + 9, { align: 'center' }); doc.text(signer.label, 55, sigY + 13, { align: 'center' }); // School Signature const director = getDirectorInfo(schoolData); const dirName = director ? director.name.toUpperCase() : schoolData.profile.name.toUpperCase(); const dirDoc = director ? `CPF: ${director.cpf}` : `CNPJ: ${schoolData.profile.cnpj || '---'}`; const dirRole = director ? director.role : 'Assinatura da Escola'; doc.line(120, sigY, 190, sigY); doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.text(dirName, 155, sigY + 5, { align: 'center' }); doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.text(dirDoc, 155, sigY + 9, { align: 'center' }); doc.text(dirRole, 155, sigY + 13, { align: 'center' }); doc.save(`ficha_matricula_${student.name.replace(/\s+/g, '_').toLowerCase()}.pdf`); }, generateStudentHistoryPDF: async (student: Student, schoolData: SchoolData) => { const doc = new jsPDF() as any; const startY = await addHeader(doc, schoolData); const payments = schoolData.payments.filter(p => p.studentId === student.id); const contracts = schoolData.contracts.filter(c => c.studentId === student.id); doc.setFontSize(16); doc.setTextColor(0); doc.text(`Histórico Acadêmico e Financeiro: ${student.name}`, 105, startY + 5, { align: 'center' }); doc.setFontSize(12); doc.setTextColor(0); doc.text('Contratos Ativos', 20, startY + 20); doc.autoTable({ startY: startY + 25, margin: { top: 45 }, didDrawPage: async (data: any) => { if (data.pageNumber > 1) await addHeader(doc, schoolData); }, head: [['Título', 'Data Emissão']], body: contracts.map(c => [ c.title, c.createdAt ? c.createdAt.split('T')[0].split('-').reverse().join('/') : '' ]), headStyles: { fillColor: [0, 0, 0] } }); const nextY = (doc as any).lastAutoTable.finalY + 15; doc.setFontSize(12); doc.text('Histórico de Pagamentos', 20, nextY); doc.autoTable({ startY: nextY + 5, margin: { top: 45 }, didDrawPage: async (data: any) => { if (data.pageNumber > 1) await addHeader(doc, schoolData); }, head: [['Descrição', 'Vencimento', 'Valor', 'Status']], body: payments.map(p => [ p.description || (p.type === 'registration' ? 'Matrícula' : 'Mensalidade'), p.dueDate ? p.dueDate.split('T')[0].split('-').reverse().join('/') : '', `R$ ${p.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`, p.status === 'paid' ? 'Pago' : p.status === 'overdue' ? 'Atrasado' : 'Pendente' ]), headStyles: { fillColor: [0, 0, 0] } }); doc.save(`historico_${student.name.replace(/\s+/g, '_').toLowerCase()}.pdf`); }, generatePaymentReceiptPDF: async (payment: Payment, student: Student, schoolData: SchoolData) => { const doc = new jsPDF() as any; doc.setDrawColor(0); doc.setLineWidth(0.5); doc.rect(10, 10, 190, 140); // Border const profile = schoolData.profile; if (schoolData.logo) { await addImageProportional(doc, schoolData.logo, 20, 15, 20, 20); } doc.setFontSize(12); doc.setTextColor(0); doc.text(profile.name, 45, 18); doc.setFontSize(8); doc.text(`CNPJ: ${profile.cnpj || '---'}`, 45, 22); doc.text(profile.address || '', 45, 26); doc.setFontSize(16); doc.setTextColor(0); doc.text('RECIBO DE PAGAMENTO', 105, 45, { align: 'center' }); doc.setFontSize(9); doc.setTextColor(0); doc.text(`Nº do Documento: ${payment.id.substring(0, 8).toUpperCase()}`, 150, 55); doc.setFontSize(11); doc.setTextColor(0); doc.text(`Recebemos de: ${student.name}`, 20, 70); doc.text(`CPF: ${student.cpf || '---'}`, 20, 76); doc.text(`A quantia de: R$ ${payment.amount.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`, 20, 85); const typeLabel = payment.type === 'registration' ? 'Taxa de Matrícula' : payment.type === 'monthly' ? 'Mensalidade do Curso' : 'Outros Serviços'; doc.text(`Referente a: ${typeLabel} ${payment.description ? `(${payment.description})` : ''}`, 20, 95); doc.text(`Data de Vencimento: ${payment.dueDate ? payment.dueDate.split('T')[0].split('-').reverse().join('/') : ''}`, 20, 105); if (payment.status === 'paid' && payment.paidDate) { doc.setFontSize(12); doc.setTextColor(0); doc.text(`PAGO EM: ${payment.paidDate}`, 105, 120, { align: 'center' }); } doc.setTextColor(0); doc.setFontSize(9); doc.text('_________________________________', 105, 140, { align: 'center' }); doc.text('Assinatura / Carimbo', 105, 145, { align: 'center' }); doc.save(`recibo_${student.name.replace(/\s+/g, '_').toLowerCase()}_${payment.id.substring(0, 4)}.pdf`); }, generateContractPDF: async (contract: Contract, student: Student, schoolData: SchoolData) => { const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }) as any; let currentY = await addHeader(doc, schoolData); // Title doc.setFontSize(14); doc.setTextColor(0); doc.setFont('helvetica', 'bold'); doc.text('CONTRATO DE PRESTAÇÃO DE SERVIÇOS EDUCACIONAIS', 105, currentY, { align: 'center' }); currentY += 10; // Contract Header Info doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text(`DATA DE EMISSÃO: ${contract.createdAt ? contract.createdAt.split('T')[0].split('-').reverse().join('/') : ''}`, 20, currentY); currentY += 6; doc.text(`CONTRATANTE: ${student.name.toUpperCase()}`, 20, currentY); currentY += 6; doc.text(`CPF: ${student.cpf || '---'}`, 20, currentY); currentY += 10; // Draw Justified Content with Pagination const margin = 20; // 2cm const pageWidth = doc.internal.pageSize.width; const maxWidth = pageWidth - (margin * 2); const lineHeight = 5.5; // 1.5 spacing approx for 10pt font currentY = await drawContractText(doc, contract.content, margin, currentY, maxWidth, lineHeight, schoolData); // Signatures const pageHeight = doc.internal.pageSize.height; const signer = getSignerInfo(student); // Check if signatures fit on current page (need about 45mm to keep them together without breaking) if (currentY > pageHeight - 45) { doc.addPage(); currentY = await addHeader(doc, schoolData); currentY += 10; } else { currentY += 25; // Extra space before signatures } // Signature Block - Unbreakable, Side by Side doc.setDrawColor(0); doc.setLineWidth(0.2); const sigY = currentY; // Signer Signature (Left Column) doc.line(20, sigY, 90, sigY); doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.text(signer.name.toUpperCase(), 55, sigY + 5, { align: 'center' }); doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.text(`CPF: ${signer.cpf || '---'}`, 55, sigY + 9, { align: 'center' }); doc.text(signer.label, 55, sigY + 13, { align: 'center' }); // School Signature (Right Column) const director = getDirectorInfo(schoolData); const dirName = director ? director.name.toUpperCase() : schoolData.profile.name.toUpperCase(); const dirDoc = director ? `CPF: ${director.cpf}` : `CNPJ: ${schoolData.profile.cnpj || '---'}`; const dirRole = director ? director.role : 'Assinatura da Escola'; doc.line(120, sigY, 190, sigY); doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.text(dirName, 155, sigY + 5, { align: 'center' }); doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.text(dirDoc, 155, sigY + 9, { align: 'center' }); doc.text(dirRole, 155, sigY + 13, { align: 'center' }); // Add Page Numbers to all pages addPageNumbers(doc); doc.save(`contrato_${contract.title.replace(/\s+/g, '_').toLowerCase()}.pdf`); }, generateClassListPDF: async (cls: Class, schoolData: SchoolData) => { const doc = new jsPDF() as any; const startY = await addHeader(doc, schoolData); const course = schoolData.courses.find(c => c.id === cls.courseId); const students = schoolData.students.filter(s => s.classId === cls.id); doc.setFontSize(16); doc.setTextColor(0); doc.text(`Relatório de Turma: ${cls.name}`, 105, startY + 5, { align: 'center' }); doc.setFontSize(10); doc.setTextColor(0); doc.text(`Curso: ${course?.name || 'N/A'}`, 20, startY + 15); doc.text(`Professor: ${cls.teacher}`, 20, startY + 22); doc.text(`Horário: ${cls.schedule}`, 20, startY + 29); doc.autoTable({ startY: startY + 35, margin: { top: 45 }, didDrawPage: async (data: any) => { if (data.pageNumber > 1) await addHeader(doc, schoolData); }, head: [['Nº', 'Nome do Aluno', 'Telefone', 'Status']], body: students.map((s, idx) => [ idx + 1, s.name, s.phone, s.status === 'active' ? 'Ativo' : 'Inativo' ]), headStyles: { fillColor: [0, 0, 0] } }); doc.save(`turma_${cls.name.replace(/\s+/g, '_').toLowerCase()}.pdf`); }, generateStudentListPDF: async (schoolData: SchoolData) => { const doc = new jsPDF() as any; const startY = await addHeader(doc, schoolData); doc.setFontSize(16); doc.setTextColor(0); doc.text('Relatório Geral de Alunos', 105, startY + 5, { align: 'center' }); doc.autoTable({ startY: startY + 15, margin: { top: 45 }, didDrawPage: async (data: any) => { if (data.pageNumber > 1) await addHeader(doc, schoolData); }, head: [['Nome', 'CPF', 'Email', 'Turma', 'Status']], body: schoolData.students.map(s => { const cls = schoolData.classes.find(c => c.id === s.classId); return [ s.name, s.cpf || '-', s.email, cls?.name || '-', s.status === 'active' ? 'Ativo' : 'Inativo' ]; }), headStyles: { fillColor: [0, 0, 0] } }); doc.save(`lista_alunos_${new Date().toISOString().split('T')[0]}.pdf`); }, generateFullSchoolReportPDF: async (schoolData: SchoolData) => { const doc = new jsPDF() as any; const startY = await addHeader(doc, schoolData); doc.setFontSize(18); doc.setTextColor(0); doc.text('Relatório Consolidado', 105, startY + 5, { align: 'center' }); doc.setFontSize(12); doc.text('Visão Geral', 20, startY + 20); doc.setFontSize(10); doc.text(`Total Alunos: ${schoolData.students.length}`, 20, startY + 28); doc.text(`Alunos Ativos: ${schoolData.students.filter(s => s.status === 'active').length}`, 20, startY + 34); doc.text(`Turmas Ativas: ${schoolData.classes.length}`, 20, startY + 40); doc.autoTable({ startY: startY + 50, margin: { top: 45 }, didDrawPage: async (data: any) => { if (data.pageNumber > 1) await addHeader(doc, schoolData); }, head: [['Alunos', 'Turmas', 'Financeiro Pago', 'Pendente']], body: [[ schoolData.students.length, schoolData.classes.length, `R$ ${schoolData.payments.filter(p => p.status === 'paid').reduce((sum, p) => sum + p.amount, 0).toFixed(2)}`, `R$ ${schoolData.payments.filter(p => p.status !== 'paid').reduce((sum, p) => sum + p.amount, 0).toFixed(2)}` ]], headStyles: { fillColor: [0, 0, 0] } }); doc.save(`relatorio_geral_${new Date().toISOString().split('T')[0]}.pdf`); }, generateCancellationTermPDF: async (student: Student, schoolData: SchoolData, cancellationReason: string) => { const doc = new jsPDF() as any; const startY = await addHeader(doc, schoolData); const cls = schoolData.classes.find(c => c.id === student.classId); const course = schoolData.courses.find(c => c.id === cls?.courseId); // Title doc.setFontSize(16); doc.setTextColor(0); doc.setFont('helvetica', 'bold'); doc.text('TERMO DE CANCELAMENTO DE MATRÍCULA', 105, startY + 10, { align: 'center' }); let currentY = startY + 25; doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Escola:', 20, currentY); doc.setFont('helvetica', 'normal'); doc.text(schoolData.profile.name || 'Microtec Informática Cursos', 38, currentY); currentY += 15; // Student Data doc.setFont('helvetica', 'bold'); doc.text('Dados do Aluno:', 20, currentY); currentY += 8; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.text(`Nome: ${student.name} | CPF: ${student.cpf || 'Não informado'}`, 20, currentY); currentY += 6; doc.text(`Curso: ${course?.name || 'Não informado'} | Turma: ${cls?.name || 'Não informado'}`, 20, currentY); currentY += 12; // Guardian Data if (student.guardianName) { doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Dados do Responsável (se menor de idade):', 20, currentY); currentY += 8; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.text(`Nome: ${student.guardianName} | CPF: ${student.guardianCpf || 'Não informado'}`, 20, currentY); currentY += 12; } // Reason doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Motivo do Cancelamento:', 20, currentY); currentY += 8; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); const splitReason = doc.splitTextToSize(cancellationReason, 170); doc.text(splitReason, 20, currentY); currentY += (splitReason.length * 6) + 10; // Term Text const termText1 = 'Pelo presente termo, o(a) aluno(a) ou seu responsável legal acima qualificado, solicita formalmente o CANCELAMENTO DA MATRÍCULA no curso especificado.'; const termText2 = 'Declara estar ciente de que o cancelamento encerra o vínculo educacional a partir desta data, não isentando o contratante de eventuais pendências financeiras adquiridas e vencidas até o presente momento, conforme contrato de prestação de serviços educacionais assinado no ato da matrícula.'; const splitTerm1 = doc.splitTextToSize(termText1, 170); doc.text(splitTerm1, 20, currentY); currentY += (splitTerm1.length * 6) + 4; const splitTerm2 = doc.splitTextToSize(termText2, 170); doc.text(splitTerm2, 20, currentY); currentY += (splitTerm2.length * 6) + 20; // Date and Signatures const dateStr = new Date().toLocaleDateString('pt-BR', { day: 'numeric', month: 'long', year: 'numeric' }); doc.text(`Redenção - CE, ${dateStr}.`, 20, currentY); currentY += 30; doc.line(20, currentY, 90, currentY); doc.line(120, currentY, 190, currentY); currentY += 5; doc.setFontSize(10); doc.text('Assinatura do Aluno ou Responsável Legal', 55, currentY, { align: 'center' }); doc.text(`${schoolData.profile.name || 'Microtec Informática Cursos'} (Administração)`, 155, currentY, { align: 'center' }); doc.save(`termo_cancelamento_${student.name.replace(/\s+/g, '_')}.pdf`); } };