relatorios

This commit is contained in:
2026-05-18 11:01:11 +01:00
parent 9835eab040
commit ae639422f5
2 changed files with 535 additions and 237 deletions

View File

@@ -1,6 +1,7 @@
// app/Professor/Alunos/relatorios.tsx
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system/legacy';
import * as Print from 'expo-print';
import { useRouter } from 'expo-router';
@@ -10,6 +11,7 @@ import { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
@@ -27,6 +29,8 @@ export default function GestaoRelatorios() {
const router = useRouter();
const [relatorios, setRelatorios] = useState<any[]>([]);
const [turmaAtiva, setTurmaAtiva] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [gerandoPDF, setGerandoPDF] = useState<string | null>(null);
@@ -42,15 +46,34 @@ export default function GestaoRelatorios() {
laranja: '#F18721',
}), [isDarkMode]);
const getBase64Image = async (imageModule: any) => {
try {
const asset = Asset.fromModule(imageModule);
await asset.downloadAsync();
const fileUri = asset.localUri || asset.uri;
if (!fileUri) return "";
const base64 = await FileSystem.readAsStringAsync(fileUri, { encoding: 'base64' });
return base64.replace(/(\r\n|\n|\r)/gm, "");
} catch (e) {
console.error("Erro no Base64:", e);
return "";
}
};
const formatarTexto = (texto: string) => {
if (!texto || texto === 'N/A') return 'N/A';
return texto.charAt(0).toUpperCase() + texto.slice(1).toLowerCase();
};
const fetchRelatorios = async (isManualRefresh = false) => {
if (!isManualRefresh) setLoading(true);
try {
const { data, error } = await supabase
.from('estagios')
.select(`
id, horas_totais, horas_concluidas, nota_final, avaliacao_url,
alunos (id, nome, turma_curso, n_escola),
empresas (nome)
id, horas_totais, horas_concluidas, horas_diarias, nota_final, avaliacao_url, data_inicio, data_fim,
alunos (id, nome, turma_curso, n_escola, ano),
empresas (nome, tutor_nome)
`)
.order('data_inicio', { ascending: false });
@@ -60,13 +83,23 @@ export default function GestaoRelatorios() {
const aluno = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos;
const empresa = Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas;
const nomeCursoFormatado = formatarTexto(aluno?.turma_curso);
const anoAluno = aluno?.ano || 10;
const turmaCompleta = `${anoAluno}º ${nomeCursoFormatado}`;
return {
id_estagio: estagio.id,
aluno_id: aluno?.id,
aluno_nome: aluno?.nome || 'Desconhecido',
turma: aluno?.turma_curso || 'N/A',
curso: nomeCursoFormatado,
ano: anoAluno,
turma: turmaCompleta,
n_escola: aluno?.n_escola || '--',
empresa_nome: empresa?.nome || 'N/A',
tutor_nome: empresa?.tutor_nome || 'N/A',
data_inicio: estagio.data_inicio,
data_fim: estagio.data_fim,
horas_diarias: estagio.horas_diarias,
horas_totais: estagio.horas_totais || 0,
horas_concluidas: estagio.horas_concluidas || 0,
nota_empresa: estagio.nota_final,
@@ -86,85 +119,294 @@ export default function GestaoRelatorios() {
useFocusEffect(useCallback(() => { fetchRelatorios(); }, []));
// 1. GERAR EXCEL (CSV) PERFEITO
const gerarExcelGeral = async () => {
try {
// O \uFEFF é o BOM (Byte Order Mark) - Obriga o Excel a reconhecer os acentos corretos!
let csvContent = "\uFEFFNumero;Aluno;Turma;Empresa;Horas Feitas;Horas Totais;Nota Empresa;Autoavaliacao\n";
relatorios.forEach(r => {
const nota = r.nota_empresa ? r.nota_empresa : 'Pendente';
const autoav = 'Pendente';
csvContent += `${r.n_escola};${r.aluno_nome};${r.turma};${r.empresa_nome};${r.horas_concluidas};${r.horas_totais};${nota};${autoav}\n`;
});
const cursosAgrupados = useMemo(() => {
const grupos: Record<string, { count10: number, count11: number, count12: number }> = {};
relatorios.forEach(r => {
if (!grupos[r.curso]) {
grupos[r.curso] = { count10: 0, count11: 0, count12: 0 };
}
if (r.ano === 10 || r.ano === 1) grupos[r.curso].count10++;
else if (r.ano === 11 || r.ano === 2) grupos[r.curso].count11++;
else if (r.ano === 12 || r.ano === 3) grupos[r.curso].count12++;
});
return Object.keys(grupos).map(curso => ({ curso, ...grupos[curso] })).sort((a, b) => a.curso.localeCompare(b.curso));
}, [relatorios]);
const relatoriosFiltrados = useMemo(() => {
if (!turmaAtiva) return [];
return relatorios.filter(r => r.turma === turmaAtiva);
}, [relatorios, turmaAtiva]);
// 🟢 FUNÇÃO AUXILIAR PARA GERAR O HTML DA MATRIZ (PARTILHADO ENTRE EXCEL E PDF)
const construirHtmlMatriz = async (logoEPVC_b64: string, logoEstagios_b64: string, isPdf = false) => {
const alunosIds = relatoriosFiltrados.map(r => r.aluno_id);
const { data: presencas } = await supabase
.from('presencas')
.select('aluno_id, data, estado, justificacao_url, estado_tutor')
.in('aluno_id', alunosIds);
let minDateStr = relatoriosFiltrados[0].data_inicio;
let maxDateStr = relatoriosFiltrados[0].data_fim;
relatoriosFiltrados.forEach(r => {
if(r.data_inicio < minDateStr) minDateStr = r.data_inicio;
if(r.data_fim > maxDateStr) maxDateStr = r.data_fim;
});
let minDate = new Date(minDateStr);
let maxDate = new Date(maxDateStr);
if (isNaN(minDate.getTime())) minDate = new Date();
if (isNaN(maxDate.getTime())) { maxDate = new Date(); maxDate.setMonth(maxDate.getMonth() + 3); }
const meses: any[] = [];
let current = new Date(minDate);
while (current <= maxDate) {
const ano = current.getFullYear();
const mes = current.getMonth();
const nomeMes = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'][mes];
const dia = current.getDate();
const dw = current.getDay();
if (dw !== 0 && dw !== 6) {
const dataStr = `${ano}-${String(mes+1).padStart(2,'0')}-${String(dia).padStart(2,'0')}`;
let mesObj = meses.find(m => m.nome === `${nomeMes} ${ano}`);
if (!mesObj) { mesObj = { nome: `${nomeMes} ${ano}`, dias: [] }; meses.push(mesObj); }
mesObj.dias.push({ dia, dataStr });
}
current.setDate(current.getDate() + 1);
}
let totalColsDias = meses.reduce((acc, m) => acc + m.dias.length + 1, 0);
if (totalColsDias === 0) totalColsDias = 1;
let mesesHtml = `<tr style="height: 35px;">
<td class="th-blue" style="width: 40px;">Nº</td>
<td class="th-blue" style="width: 220px;">Nome do Aluno</td>`;
let diasHtml = `<tr style="height: 25px;">
<td class="th-blue"></td><td class="th-blue"></td>`;
meses.forEach((m: any) => {
mesesHtml += `<td colspan="${m.dias.length + 1}" class="th-orange">${m.nome}</td>`;
m.dias.forEach((d: any) => {
diasHtml += `<td class="th-blue" style="width: 24px;">${d.dia}</td>`;
});
diasHtml += `<td class="th-total" style="width: 35px;">Total</td>`;
});
mesesHtml += `<td class="th-orange" style="width: 100px;">Horas Feitas (1)</td>
<td class="th-orange" style="width: 100px;">Horas por Lecionar (2)</td>
<td class="th-orange" style="width: 100px;">Total Contrato (3)</td></tr>`;
diasHtml += `<td class="th-blue"></td><td class="th-blue"></td><td class="th-blue"></td></tr>`;
let alunosHtml = '';
relatoriosFiltrados.forEach((r, idx) => {
alunosHtml += `<tr class="${idx % 2 === 0 ? 'zebra' : ''}" style="height: 25px;">
<td class="td-cell">${r.n_escola}</td>
<td class="td-name">${r.aluno_nome}</td>`;
let totalFeitasGlobais = 0;
const horasDiarias = parseInt(String(r.horas_diarias || '8').match(/\d+/)?.[0] || '8');
meses.forEach((m: any) => {
let totalMesFeitas = 0;
m.dias.forEach((d: any) => {
const p = presencas?.find(x => x.aluno_id === r.aluno_id && x.data === d.dataStr);
if (p) {
if (p.estado === 'presente') {
totalMesFeitas += horasDiarias;
alunosHtml += `<td class="td-cell" style="background-color: #E6F2F5;">${horasDiarias}</td>`;
} else if (p.estado === 'faltou') {
const isJustificada = p.justificacao_url || p.estado_tutor === 'aprovado';
const label = isJustificada ? 'F' : 'FI';
const color = isJustificada ? '#D97706' : '#EF4444';
alunosHtml += `<td class="td-cell" style="color: ${color}; font-weight: bold;">${label}</td>`;
} else {
alunosHtml += `<td class="td-cell"></td>`;
}
} else {
alunosHtml += `<td class="td-cell"></td>`;
}
});
totalFeitasGlobais += totalMesFeitas;
alunosHtml += `<td class="td-total-cell">${totalMesFeitas}</td>`;
});
const faltam = Math.max(0, (r.horas_totais || 400) - totalFeitasGlobais);
alunosHtml += `
<td class="td-cell" style="font-weight:bold; color: #003049;">${totalFeitasGlobais}</td>
<td class="td-cell" style="font-weight:bold; color: #EF4444;">${faltam}</td>
<td class="td-cell" style="font-weight:bold;">${r.horas_totais || 400}</td>
</tr>`;
});
const imgSrcEscola = isPdf ? `data:image/png;base64,${logoEPVC_b64}` : 'cid:logo_escola';
const imgSrcApp = isPdf ? `data:image/png;base64,${logoEstagios_b64}` : 'cid:logo_app';
return {
totalColsDias,
minDate,
maxDate,
htmlCorpo: `
<table class="main-table">
<tr>
<td colspan="3" rowspan="2" style="text-align: center; vertical-align: middle;">
<img src="${imgSrcEscola}" width="130" height="60" />
</td>
<td colspan="${totalColsDias - 1}" rowspan="2" class="title-escola" style="text-align: center; vertical-align: middle;">
Escola Profissional de Vila do Conde<br/>
<span style="font-size: 14pt; color: #f18721;">Mapa Oficial de Assiduidade (FCT)</span>
</td>
<td colspan="3" rowspan="2" style="text-align: center; vertical-align: middle;">
<img src="${imgSrcApp}" width="130" height="60" />
</td>
</tr>
<tr></tr>
<tr><td colspan="${totalColsDias + 5}"></td></tr>
<tr>
<td colspan="3" class="subtitle">Período: ${new Date(minDate).toLocaleDateString('pt-PT')} a ${new Date(maxDate).toLocaleDateString('pt-PT')}</td>
<td colspan="${totalColsDias - 1}" class="subtitle" style="text-align: center;">Curso / Turma: ${turmaAtiva || 'Todas as Turmas'}</td>
<td colspan="3" class="subtitle" style="text-align: right;">Ano Letivo: 2025/2026</td>
</tr>
<tr><td colspan="${totalColsDias + 5}"></td></tr>
${mesesHtml}
${diasHtml}
${alunosHtml}
<tr><td colspan="${totalColsDias + 5}"></td></tr>
<tr>
<td colspan="5" style="font-style: italic; color: #64748b;">(1) - Horas Lecionadas no período</td>
<td colspan="5" style="font-style: italic; color: #64748b;">(2) - Horas em falta para lecionar</td>
<td colspan="5" style="font-style: italic; color: #64748b;">(3) - Total de Horas do Contrato de Estágio</td>
</tr>
<tr>
<td colspan="15" style="font-style: italic; color: #ef4444; font-weight: bold;">
Legenda de Faltas: [ F ] - Falta Justificada | [ FI ] - Falta Injustificada
</td>
</tr>
</table>
`
};
};
// 1. EXPORTAR MATRIZ EM EXCEL (FORMATO MHTML COM IMAGENS FIXAS)
const gerarExcelGeral = async () => {
if (relatoriosFiltrados.length === 0) return Alert.alert("Aviso", "Não há alunos para exportar nesta turma.");
setRefreshing(true);
try {
const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc2.png'));
const b64App = await getBase64Image(require('../../../assets/images/logo.png'));
const { totalColsDias, htmlCorpo } = await construirHtmlMatriz(b64Escola, b64App, false);
const mhtmlExcel = `MIME-Version: 1.0
Content-Type: multipart/related; boundary="----=_NextPart_01D1"
------=_NextPart_01D1
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 7bit
<html>
<head>
<meta charset="utf-8" />
<style>
.main-table { border-collapse: collapse; font-family: 'Segoe UI', Calibri, sans-serif; }
.th-blue { background-color: #003049; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; }
.th-orange { background-color: #f18721; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; }
.th-total { background-color: #0f172a; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; }
.td-cell { border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; mso-number-format: "0"; }
.td-total-cell { border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-weight: bold; background-color: #e2e8f0; }
.td-name { border: 1px solid #cbd5e1; text-align: left; vertical-align: middle; font-weight: bold; white-space: nowrap; padding-left: 5px; }
.title-escola { font-size: 18pt; font-weight: bold; color: #003049; }
.subtitle { font-size: 11pt; color: #64748b; font-weight: bold; }
.zebra { background-color: #F8FAFC; }
</style>
</head>
<body>
${htmlCorpo}
</body>
</html>
------=_NextPart_01D1
Content-Type: image/png
Content-ID: <logo_escola>
Content-Transfer-Encoding: base64
${b64Escola}
------=_NextPart_01D1
Content-Type: image/png
Content-ID: <logo_app>
Content-Transfer-Encoding: base64
${b64App}
------=_NextPart_01D1--`;
const fileNameTurma = turmaAtiva ? turmaAtiva.replace(/[^a-zA-Z0-9]/g, '') : 'Geral';
const fileUri = FileSystem.documentDirectory + `Mapa_Assiduidade_${fileNameTurma}.xls`;
await FileSystem.writeAsStringAsync(fileUri, mhtmlExcel, { encoding: FileSystem.EncodingType.UTF8 });
const fileName = `Pauta_Estagios_${new Date().toISOString().split('T')[0]}.csv`;
const fileUri = FileSystem.documentDirectory + fileName;
await FileSystem.writeAsStringAsync(fileUri, csvContent, { encoding: FileSystem.EncodingType.UTF8 });
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(fileUri, { mimeType: 'text/csv', dialogTitle: 'Exportar Pauta Excel' });
await Sharing.shareAsync(fileUri, {
mimeType: 'application/vnd.ms-excel',
dialogTitle: 'Exportar Pauta de Assiduidade',
UTI: 'com.microsoft.excel.xls'
});
}
} catch (e) {
Alert.alert('Erro', 'Falha ao gerar o Excel.');
console.error(e);
Alert.alert('Erro', 'Falha ao exportar a Matriz Excel.');
} finally {
setRefreshing(false);
}
};
// 2. GERAR PAUTA GLOBAL EM PDF
// 2. 🟢 EXPORTAR A MESMA MATRIZ EM FORMATO PDF (COMPLETAMENTE CORRIGIDO!)
const gerarPautaPDF = async () => {
if (relatoriosFiltrados.length === 0) return Alert.alert("Aviso", "Não há alunos para exportar nesta turma.");
setGerandoPDF("pauta_global");
try {
let linhasTabela = '';
relatorios.forEach(r => {
const nota = r.nota_empresa ? `<strong>${r.nota_empresa}</strong>` : '<span style="color:#F18721">Pendente</span>';
linhasTabela += `
<tr>
<td style="text-align: center;">${r.n_escola}</td>
<td>${r.aluno_nome}</td>
<td>${r.turma}</td>
<td>${r.empresa_nome}</td>
<td style="text-align: center;">${r.horas_concluidas}/${r.horas_totais}h</td>
<td style="text-align: center;">${nota}</td>
</tr>
`;
});
const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc2.png'));
const b64App = await getBase64Image(require('../../../assets/images/logo.png'));
const htmlContent = `
const { htmlCorpo } = await construirHtmlMatriz(b64Escola, b64App, true);
// Usamos uma folha horizontal (Landscape) e uma tabela responsiva para caber no PDF
const htmlCompletoPDF = `
<!DOCTYPE html>
<html>
<html lang="pt-PT">
<head>
<meta charset="utf-8" />
<meta charset="UTF-8">
<style>
@page { margin: 15mm; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #1e293b; font-size: 12px; }
h1 { color: #003049; text-transform: uppercase; font-size: 20px; border-bottom: 2px solid #71BEB3; padding-bottom: 10px; text-align: center; }
.info { text-align: center; margin-bottom: 20px; font-size: 14px; color: #64748B; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th { background-color: #003049; color: white; padding: 10px; text-align: left; }
td { border: 1px solid #CBD5E1; padding: 8px; }
tr:nth-child(even) { background-color: #F8FAFC; }
@page { size: A4 landscape; margin: 8mm; }
body { font-family: 'Segoe UI', Helvetica, sans-serif; margin: 0; padding: 0; color: #1E293B; background-color: #fff; font-size: 8px; }
.main-table { width: 100%; border-collapse: collapse; }
.th-blue { background-color: #003049; color: #ffffff; font-weight: bold; border: 0.5px solid #cbd5e1; text-align: center; vertical-align: middle; padding: 3px 1px; }
.th-orange { background-color: #f18721; color: #ffffff; font-weight: bold; border: 0.5px solid #cbd5e1; text-align: center; vertical-align: middle; padding: 3px 1px; }
.th-total { background-color: #0f172a; color: #ffffff; font-weight: bold; border: 0.5px solid #cbd5e1; text-align: center; vertical-align: middle; padding: 3px 1px; }
.td-cell { border: 0.5px solid #cbd5e1; text-align: center; vertical-align: middle; padding: 3px 1px; }
.td-total-cell { border: 0.5px solid #cbd5e1; text-align: center; vertical-align: middle; font-weight: bold; background-color: #e2e8f0; padding: 3px 1px; }
.td-name { border: 0.5px solid #cbd5e1; text-align: left; vertical-align: middle; font-weight: bold; padding: 3px 4px; white-space: nowrap; }
.title-escola { font-size: 14pt; font-weight: bold; color: #003049; line-height: 1.3; }
.subtitle { font-size: 9pt; color: #64748b; font-weight: bold; }
.zebra { background-color: #F8FAFC; }
</style>
</head>
<body>
<h1>Pauta Geral de Avaliações - Estágios</h1>
<div class="info">Data de Emissão: ${new Date().toLocaleDateString('pt-PT')}</div>
<table>
<tr>
<th style="text-align: center; width: 5%;">Nº</th>
<th style="width: 30%;">Nome do Aluno</th>
<th style="width: 15%;">Turma/Curso</th>
<th style="width: 25%;">Empresa</th>
<th style="text-align: center; width: 15%;">Horas</th>
<th style="text-align: center; width: 10%;">Nota Final</th>
</tr>
${linhasTabela}
</table>
${htmlCorpo}
</body>
</html>
`;
const { uri } = await Print.printToFileAsync({ html: htmlContent });
const newFileUri = `${FileSystem.documentDirectory}Pauta_Geral_Estagios.pdf`;
const { uri } = await Print.printToFileAsync({ html: htmlCompletoPDF });
const fileNameTurma = turmaAtiva ? turmaAtiva.replace(/[^a-zA-Z0-9]/g, '') : 'Geral';
const newFileUri = `${FileSystem.documentDirectory}Pauta_Oficial_${fileNameTurma}.pdf`;
await FileSystem.moveAsync({ from: uri, to: newFileUri });
@@ -173,107 +415,83 @@ export default function GestaoRelatorios() {
}
} catch (e) {
console.error(e);
Alert.alert('Erro', 'Falha ao gerar a Pauta em PDF.');
Alert.alert('Erro', 'Falha ao gerar o PDF Oficial.');
} finally {
setGerandoPDF(null);
}
};
// 3. GERAR PDF DOS SUMÁRIOS DIÁRIOS
const gerarSumariosPDF = async (estagio_id: string, aluno_id: string, aluno_nome: string) => {
setGerandoPDF(estagio_id);
const gerarSumariosPDF = async (r: any) => {
setGerandoPDF(r.id_estagio);
try {
let { data: sumarios, error } = await supabase
.from('registos_diarios')
.select('data, tipo, sumario')
.eq('estagio_id', estagio_id)
.order('data', { ascending: true });
let { data: sumarios, error } = await supabase.from('registos_diarios').select('data, tipo, sumario').eq('estagio_id', r.id_estagio).order('data', { ascending: true });
if (error) throw error;
if (!sumarios || sumarios.length === 0) {
if (!aluno_id) throw new Error("ID do aluno não encontrado.");
const { data: presencas, error: presencasErr } = await supabase
.from('presencas')
.select('data, estado, sumario')
.eq('aluno_id', aluno_id)
.order('data', { ascending: true });
if (!r.aluno_id) throw new Error("ID do aluno não encontrado.");
const { data: presencas, error: presencasErr } = await supabase.from('presencas').select('data, estado, sumario').eq('aluno_id', r.aluno_id).order('data', { ascending: true });
if (presencasErr) throw presencasErr;
if (presencas && presencas.length > 0) {
sumarios = presencas.map(p => ({
data: p.data,
tipo: p.estado || 'Presença',
sumario: p.sumario
}));
}
if (presencas && presencas.length > 0) sumarios = presencas.map(p => ({ data: p.data, tipo: p.estado || 'Presença', sumario: p.sumario }));
}
if (!sumarios || sumarios.length === 0) {
Alert.alert('Aviso', 'Não foram encontrados registos diários nem presenças para este aluno.');
Alert.alert('Aviso', 'Não foram encontrados registos para este aluno.');
setGerandoPDF(null);
return;
}
let linhasTabela = '';
sumarios.forEach((s: any) => {
const dataFormatada = new Date(s.data).toLocaleDateString('pt-PT');
linhasTabela += `
<tr>
<td>${dataFormatada}</td>
<td><strong>${s.tipo}</strong></td>
<td>${s.sumario || 'Sem descrição submetida.'}</td>
</tr>
`;
linhasTabela += `<tr><td style="text-align: center;">${new Date(s.data).toLocaleDateString('pt-PT')}</td><td style="text-align: center;"><strong>${s.tipo}</strong></td><td>${s.sumario || '<em>Sem descrição.</em>'}</td></tr>`;
});
const logoEPVC_b64 = await getBase64Image(require('../../../assets/images/logoepvc2.png'));
const logoEstagios_b64 = await getBase64Image(require('../../../assets/images/logo.png'));
const htmlContent = `
<!DOCTYPE html>
<html>
<html lang="pt-PT">
<head>
<meta charset="utf-8" />
<style>
@page { margin: 15mm; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 0; color: #1e293b; line-height: 1.4; }
h1 { color: #003049; text-transform: uppercase; font-size: 22px; border-bottom: 2px solid #71BEB3; padding-bottom: 10px; margin-bottom: 5px; }
h2 { color: #F18721; font-size: 15px; margin-bottom: 25px; margin-top: 0; }
table { width: 100%; border-collapse: collapse; font-size: 11px; }
th { background-color: #003049; color: white; padding: 10px; text-align: left; }
td { border: 1px solid #CBD5E1; padding: 10px; vertical-align: top; }
tr:nth-child(even) { background-color: #F8FAFC; }
@page { size: A4; margin: 0; } html, body { height: 99%; overflow: hidden; }
body { font-family: sans-serif; margin: 0; padding: 12mm 15mm; color: #1E293B; font-size: 10px; }
.header-table { width: 100%; border-bottom: 2px solid #003049; padding-bottom: 5px; margin-bottom: 8px; }
.logo-epvc { width: 110px; } .logo-estagios { width: 160px; float: right; margin-top: -5px; }
.header-center { text-align: center; } .header-center h1 { color: #003049; margin: 0; font-size: 15px; } .header-center h2 { color: #F18721; margin: 2px 0 0; font-size: 9px; }
.info-table { width: 100%; border-collapse: collapse; margin-bottom: 10px; font-size: 9px; border: 1px solid #CBD5E1; }
.info-table td { padding: 4px 6px; border: 1px solid #CBD5E1; } .info-table td.label { background-color: #F8FAFC; font-weight: bold; width: 15%; }
.section-title { background-color: #003049; color: white; padding: 4px 8px; font-size: 9px; font-weight: bold; margin-bottom: 5px; }
.eval-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 9px; } .eval-table th { background-color: #F1F5F9; padding: 4px; border: 1px solid #CBD5E1; } .eval-table td { border: 1px solid #CBD5E1; padding: 3px 8px; }
.footer { text-align: center; padding-top: 5px; border-top: 1px solid #E2E8F0; position: absolute; bottom: 10mm; left: 0; right: 0; margin: 0 auto; width: calc(100% - 30mm); }
.banner-img { max-height: 30px; margin-bottom: 3px; } .footer-text { font-size: 8px; color: #94A3B8; }
</style>
</head>
<body>
<h1>Diário de Bordo</h1>
<h2>Estagiário: ${aluno_nome}</h2>
<table>
<table class="header-table">
<tr>
<th style="width: 15%;">Data</th>
<th style="width: 20%;">Natureza/Estado</th>
<th style="width: 65%;">Atividades Desenvolvidas</th>
<td style="width: 25%;"><img src="data:image/png;base64,${logoEPVC_b64}" class="logo-epvc" /></td>
<td style="width: 50%;" class="header-center"><h1>Diário de Bordo</h1><h2>Formação em Contexto de Trabalho</h2></td>
<td style="width: 25%; text-align: right;"><img src="data:image/png;base64,${logoEstagios_b64}" class="logo-estagios" /></td>
</tr>
${linhasTabela}
</table>
<table class="info-table">
<tr><td class="label">Estagiário:</td><td><strong>${r.aluno_nome}</strong></td><td class="label">Turma:</td><td>${r.turma}</td></tr>
<tr><td class="label">Entidade:</td><td><strong>${r.empresa_nome}</strong></td><td class="label">Tutor(a):</td><td>${r.tutor_nome}</td></tr>
</table>
<div class="section-title">Registo de Atividades Diárias</div>
<table class="eval-table"><tr><th style="width: 15%;">Data</th><th style="width: 20%;">Natureza</th><th style="width: 65%;">Sumário</th></tr>${linhasTabela}</table>
<div class="footer"><img src="data:image/png;base64,${logoEPVC_b64}" class="banner-img" /><div class="footer-text">Documento gerado digitalmente via plataforma Estágios+ EPVC. Data: ${new Date().toLocaleDateString('pt-PT')}</div></div>
</body>
</html>
`;
const { uri } = await Print.printToFileAsync({ html: htmlContent });
const safeName = aluno_nome.replace(/[^a-zA-Z0-9]/g, '_');
const safeName = r.aluno_nome.replace(/[^a-zA-Z0-9]/g, '_');
const newFileUri = `${FileSystem.documentDirectory}Diario_Bordo_${safeName}.pdf`;
await FileSystem.moveAsync({ from: uri, to: newFileUri });
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(newFileUri, {
mimeType: 'application/pdf',
dialogTitle: `Partilhar Diário de Bordo de ${aluno_nome}`,
UTI: 'com.adobe.pdf'
});
}
if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' });
} catch (e) {
console.error("Erro a gerar Sumários PDF:", e);
console.error(e);
Alert.alert('Erro', 'Não foi possível ler os registos.');
} finally {
setGerandoPDF(null);
@@ -284,11 +502,16 @@ export default function GestaoRelatorios() {
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => router.back()}>
<View style={[styles.header, { paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 15 : 35 }]}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => {
if (turmaAtiva) setTurmaAtiva(null);
else router.back();
}}>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Gestão de Avaliações</Text>
<Text style={[styles.headerTitle, { color: cores.texto }]}>
{turmaAtiva ? turmaAtiva : 'Relatórios por Turma'}
</Text>
<View style={{ width: 24 }} />
</View>
@@ -297,91 +520,156 @@ export default function GestaoRelatorios() {
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchRelatorios(true); }} tintColor={cores.azulMarinho} />}
>
{/* BOTÕES DE EXPORTAÇÃO GLOBAL */}
<View style={styles.botoesGlobaisContainer}>
<TouchableOpacity style={[styles.btnGlobal, { backgroundColor: cores.verdeAgua + '30', borderColor: cores.verdeAgua }]} onPress={gerarExcelGeral}>
<Ionicons name="grid-outline" size={26} color="#003049" />
<Text style={[styles.btnGlobalText, { color: '#003049' }]}>Excel (CSV)</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btnGlobal, { backgroundColor: cores.laranja + '20', borderColor: cores.laranja }]} onPress={gerarPautaPDF}>
<Ionicons name="document-text" size={26} color={cores.laranja} />
<Text style={[styles.btnGlobalText, { color: cores.laranja }]}>Pauta em PDF</Text>
</TouchableOpacity>
</View>
<Text style={styles.sectionTitle}>Processos Individuais</Text>
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azulMarinho} style={{ marginTop: 50 }} />
) : relatorios.length === 0 ? (
<Text style={{ textAlign: 'center', color: cores.textoSecundario, marginTop: 50 }}>Nenhum estágio encontrado.</Text>
) : (
relatorios.map((r, index) => (
<View key={index} style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.cardHeader}>
<View style={[styles.avatar, { backgroundColor: cores.azulMarinho }]}>
<Text style={{ color: '#FFF', fontWeight: 'bold', fontSize: 18 }}>{r.aluno_nome.charAt(0)}</Text>
) : !turmaAtiva ? (
<View>
{cursosAgrupados.map((cursoGrupo, index) => (
<View key={index} style={styles.cursoSection}>
<View style={styles.cursoHeader}>
<Ionicons name="folder-open" size={20} color={cores.laranja} />
<Text style={[styles.cursoTitle, { color: cores.texto }]}>{cursoGrupo.curso}</Text>
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.alunoName, { color: cores.texto }]}>{r.aluno_nome}</Text>
<Text style={[styles.alunoSub, { color: cores.textoSecundario }]}>{r.empresa_nome} {r.turma}</Text>
</View>
</View>
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
{/* 1. AVALIAÇÃO DA EMPRESA */}
<View style={styles.moduloBox}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>1. Avaliação da Empresa</Text>
{r.nota_empresa ? (
<Text style={[styles.notaTag, { backgroundColor: cores.azulMarinho + '20', color: cores.azulMarinho }]}>{r.nota_empresa} Val.</Text>
) : (
<Text style={[styles.notaTag, { backgroundColor: cores.laranja + '20', color: cores.laranja }]}>Pendente</Text>
)}
</View>
{r.pdf_empresa && (
<TouchableOpacity style={styles.btnAcaoLigeiro} onPress={() => WebBrowser.openBrowserAsync(r.pdf_empresa)}>
<Ionicons name="document-text" size={16} color={cores.azulMarinho} />
<Text style={[styles.textoAcao, { color: cores.azulMarinho }]}>Ver Ficha de Avaliação</Text>
<View style={styles.cardsRow}>
<TouchableOpacity
style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count10 > 0 ? 1 : 0.5 }]}
activeOpacity={0.7}
disabled={cursoGrupo.count10 === 0}
onPress={() => setTurmaAtiva(`10º ${cursoGrupo.curso}`)}
>
<Text style={[styles.turmaAno, { color: cores.texto }]}>10º</Text>
<Text style={[styles.turmaCount, { color: cores.textoSecundario }]}>{cursoGrupo.count10} Aluno(s)</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count11 > 0 ? 1 : 0.5 }]}
activeOpacity={0.7}
disabled={cursoGrupo.count11 === 0}
onPress={() => setTurmaAtiva(`11º ${cursoGrupo.curso}`)}
>
<Text style={[styles.turmaAno, { color: cores.texto }]}>11º</Text>
<Text style={[styles.turmaCount, { color: cores.textoSecundario }]}>{cursoGrupo.count11} Aluno(s)</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count12 > 0 ? 1 : 0.5 }]}
activeOpacity={0.7}
disabled={cursoGrupo.count12 === 0}
onPress={() => setTurmaAtiva(`12º ${cursoGrupo.curso}`)}
>
<Text style={[styles.turmaAno, { color: cores.texto }]}>12º</Text>
<Text style={[styles.turmaCount, { color: cores.textoSecundario }]}>{cursoGrupo.count12} Aluno(s)</Text>
</TouchableOpacity>
</View>
</View>
))}
{cursosAgrupados.length === 0 && (
<Text style={{ textAlign: 'center', color: cores.textoSecundario, marginTop: 50 }}>Nenhum estágio em sistema.</Text>
)}
</View>
) : (
<View>
<View style={styles.botoesGlobaisContainer}>
<TouchableOpacity style={[styles.btnGlobal, { backgroundColor: cores.verdeAgua + '30', borderColor: cores.verdeAgua }]} onPress={gerarExcelGeral}>
<Ionicons name="grid-outline" size={26} color="#003049" />
<Text style={[styles.btnGlobalText, { color: '#003049' }]}>Matriz de Assiduidade</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btnGlobal, { backgroundColor: cores.laranja + '20', borderColor: cores.laranja }]}
onPress={gerarPautaPDF}
disabled={gerandoPDF === "pauta_global"}
>
{gerandoPDF === "pauta_global" ? (
<ActivityIndicator size="small" color={cores.laranja} />
) : (
<>
<Ionicons name="document-text" size={26} color={cores.laranja} />
<Text style={[styles.btnGlobalText, { color: cores.laranja }]}>Pauta em PDF</Text>
</>
)}
</View>
{/* 2. AUTOAVALIAÇÃO DO ALUNO */}
<View style={styles.moduloBox}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>2. Autoavaliação do Aluno</Text>
<Text style={[styles.notaTag, { backgroundColor: cores.textoSecundario + '20', color: cores.textoSecundario }]}>Em Breve</Text>
</View>
</View>
{/* 3. DIÁRIO DE BORDO (PDF) - AGORA COM O ALUNO_ID! */}
<View style={[styles.moduloBox, { borderBottomWidth: 0, paddingBottom: 0, marginBottom: 0 }]}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>3. Registos Diários</Text>
<Text style={[styles.notaTag, { backgroundColor: cores.verdeAgua + '30', color: '#003049' }]}>{r.horas_concluidas}h Registadas</Text>
</View>
<TouchableOpacity
style={[styles.btnAcaoLigeiro, { borderColor: cores.laranja }]}
onPress={() => gerarSumariosPDF(r.id_estagio, r.aluno_id, r.aluno_nome)}
disabled={gerandoPDF === r.id_estagio}
>
{gerandoPDF === r.id_estagio ? (
<ActivityIndicator size="small" color={cores.laranja} />
) : (
<>
<Ionicons name="calendar-outline" size={16} color={cores.laranja} />
<Text style={[styles.textoAcao, { color: cores.laranja }]}>Exportar Histórico (PDF)</Text>
</>
)}
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
))
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15, marginLeft: 5 }}>
<Text style={styles.sectionTitle}>Processos de {turmaAtiva}</Text>
<Text style={{ fontSize: 11, fontWeight: '700', color: cores.textoSecundario }}>{relatoriosFiltrados.length} Aluno(s)</Text>
</View>
{relatoriosFiltrados.length === 0 ? (
<View style={{ alignItems: 'center', marginTop: 50 }}>
<Ionicons name="folder-open-outline" size={40} color={cores.borda} style={{ marginBottom: 10 }} />
<Text style={{ textAlign: 'center', color: cores.textoSecundario, fontWeight: '600' }}>Nenhum estágio para esta turma.</Text>
</View>
) : (
relatoriosFiltrados.map((r, index) => (
<View key={index} style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.cardHeader}>
<View style={[styles.avatar, { backgroundColor: cores.azulMarinho }]}>
<Text style={{ color: '#FFF', fontWeight: 'bold', fontSize: 18 }}>{r.aluno_nome.charAt(0)}</Text>
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.alunoName, { color: cores.texto }]}>{r.aluno_nome}</Text>
<Text style={[styles.alunoSub, { color: cores.textoSecundario }]}>{r.empresa_nome}</Text>
</View>
</View>
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
<View style={styles.moduloBox}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>1. Avaliação da Empresa</Text>
{r.nota_empresa ? (
<Text style={[styles.notaTag, { backgroundColor: cores.azulMarinho + '20', color: cores.azulMarinho }]}>{r.nota_empresa} Val.</Text>
) : (
<Text style={[styles.notaTag, { backgroundColor: cores.laranja + '20', color: cores.laranja }]}>Pendente</Text>
)}
</View>
{r.pdf_empresa && (
<TouchableOpacity style={styles.btnAcaoLigeiro} onPress={() => WebBrowser.openBrowserAsync(r.pdf_empresa)}>
<Ionicons name="document-text" size={16} color={cores.azulMarinho} />
<Text style={[styles.textoAcao, { color: cores.azulMarinho }]}>Ver Ficha de Avaliação</Text>
</TouchableOpacity>
)}
</View>
<View style={styles.moduloBox}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>2. Autoavaliação do Aluno</Text>
<Text style={[styles.notaTag, { backgroundColor: cores.textoSecundario + '20', color: cores.textoSecundario }]}>Em Breve</Text>
</View>
</View>
<View style={[styles.moduloBox, { borderBottomWidth: 0, paddingBottom: 0, marginBottom: 0 }]}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>3. Diário de Bordo</Text>
<Text style={[styles.notaTag, { backgroundColor: cores.verdeAgua + '30', color: '#003049' }]}>{r.horas_concluidas}h Registadas</Text>
</View>
<TouchableOpacity
style={[styles.btnAcaoLigeiro, { borderColor: cores.laranja }]}
onPress={() => gerarSumariosPDF(r)}
disabled={gerandoPDF === r.id_estagio}
>
{gerandoPDF === r.id_estagio ? (
<ActivityIndicator size="small" color={cores.laranja} />
) : (
<>
<Ionicons name="calendar-outline" size={16} color={cores.laranja} />
<Text style={[styles.textoAcao, { color: cores.laranja }]}>Exportar Diário (PDF)</Text>
</>
)}
</TouchableOpacity>
</View>
</View>
))
)}
</View>
)}
</ScrollView>
@@ -391,21 +679,31 @@ export default function GestaoRelatorios() {
const styles = StyleSheet.create({
safeArea: { flex: 1 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 15, paddingBottom: 10 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 15 },
btnVoltar: { padding: 5, marginLeft: -5 },
headerTitle: { fontSize: 18, fontWeight: '900' },
scrollContent: { padding: 20, paddingBottom: 40 },
cursoSection: { marginBottom: 30 },
cursoHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15, gap: 8 },
cursoTitle: { fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: -0.5 },
cardsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 10 },
turmaCard: { flex: 1, paddingVertical: 20, borderRadius: 20, borderWidth: 1, alignItems: 'center', justifyContent: 'center', elevation: 1, shadowOpacity: 0.05, shadowRadius: 5 },
turmaAno: { fontSize: 24, fontWeight: '900', marginBottom: 4 },
turmaCount: { fontSize: 11, fontWeight: '700' },
botoesGlobaisContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 25, gap: 15 },
btnGlobal: { flex: 1, flexDirection: 'column', alignItems: 'center', paddingVertical: 18, borderRadius: 20, borderWidth: 1.5, elevation: 1 },
btnGlobalText: { fontSize: 14, fontWeight: '900', marginTop: 8 },
btnGlobal: { flex: 1, flexDirection: 'column', alignItems: 'center', paddingVertical: 18, borderRadius: 20, borderWidth: 1.5, elevation: 0 },
btnGlobalText: { fontSize: 13, fontWeight: '900', marginTop: 8 },
sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 15, marginLeft: 5, letterSpacing: 1 },
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', letterSpacing: 1 },
card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 20 },
card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 20, elevation: 1, shadowOpacity: 0.05, shadowRadius: 10 },
cardHeader: { flexDirection: 'row', alignItems: 'center' },
avatar: { width: 44, height: 44, borderRadius: 22, justifyContent: 'center', alignItems: 'center' },
alunoName: { fontSize: 16, fontWeight: '900' },
alunoName: { fontSize: 16, fontWeight: '900', letterSpacing: -0.5 },
alunoSub: { fontSize: 12, fontWeight: '600', marginTop: 2 },
divider: { height: 1, marginVertical: 15 },