From 4d6a460ee4df155f8c9f16b31014055bc0d3bdd8 Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Thu, 11 Jun 2026 11:50:06 +0100 Subject: [PATCH] updates --- app/Empresas/avaliacoesEmpresa.tsx | 52 ++- app/Empresas/detalhesAluno.tsx | 103 +++-- app/Empresas/fichaAvaliacao.tsx | 382 +++++++++++++---- app/Professor/Alunos/relatorios.tsx | 636 ++++++++++++++-------------- 4 files changed, 728 insertions(+), 445 deletions(-) diff --git a/app/Empresas/avaliacoesEmpresa.tsx b/app/Empresas/avaliacoesEmpresa.tsx index d2d82aa..f4d3434 100644 --- a/app/Empresas/avaliacoesEmpresa.tsx +++ b/app/Empresas/avaliacoesEmpresa.tsx @@ -4,13 +4,13 @@ import { useFocusEffect } from '@react-navigation/native'; import { useRouter } from 'expo-router'; import { useCallback, useMemo, useState } from 'react'; import { - ActivityIndicator, - FlatList, - StatusBar, - StyleSheet, - Text, - TouchableOpacity, - View + ActivityIndicator, + FlatList, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; @@ -48,36 +48,43 @@ export default function AvaliacoesEmpresaLista() { if (!empresa) return; - // Vai buscar os estagios desta empresa e cruza com os nomes dos alunos + // CORREÇÃO: Vamos buscar a nota diretamente da tabela 'avaliacoes_empresa' através do relacionamento const { data, error } = await supabase .from('estagios') .select(` id, - nota_final, - alunos (id, nome) + alunos (id, nome), + avaliacoes_empresa (nota_final) `) .eq('empresa_id', empresa.id); if (error) throw error; if (data) { - // Formatar os dados para a lista, lidando com o picuinhas do TypeScript const listaFormatada = data.map((estagio: any) => { - // Extrair o aluno em segurança, quer venha como objeto ou como lista (Array) const aluno = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos; + + // Extrai a avaliação em segurança (visto que pode vir como objeto único ou array dependendo da relação da BD) + const avaliacao = Array.isArray(estagio.avaliacoes_empresa) + ? estagio.avaliacoes_empresa[0] + : estagio.avaliacoes_empresa; + + // Obtém a nota real guardada no Supabase + const notaReal = avaliacao?.nota_final; + const foiAvaliado = notaReal !== null && notaReal !== undefined; return { estagio_id: estagio.id, aluno_id: aluno?.id, aluno_nome: aluno?.nome || 'Aluno Desconhecido', - nota: estagio.nota_final, - avaliado: estagio.nota_final !== null && estagio.nota_final !== undefined + nota: foiAvaliado ? Number(notaReal) : null, + avaliado: foiAvaliado }; }); setEstagios(listaFormatada); } } catch (error) { - console.error(error); + console.error('Erro ao listar avaliações:', error); } finally { setLoading(false); } @@ -91,7 +98,13 @@ export default function AvaliacoesEmpresaLista() { style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]} onPress={() => router.push({ pathname: '/Empresas/fichaAvaliacao', - params: { estagio_id: item.estagio_id, aluno_nome: item.aluno_nome, nota_atual: item.nota } + // Passamos também os dados adicionais necessários para preencher a ficha corretamente + params: { + estagio_id: item.estagio_id, + aluno_nome: item.aluno_nome, + aluno_turma: item.aluno_turma || '', + n_escola: item.aluno_id || '' + } })} > @@ -99,14 +112,17 @@ export default function AvaliacoesEmpresaLista() { - - {item.aluno_nome} + + + {item.aluno_nome} + {item.avaliado ? '✓ Avaliação Concluída' : 'Aguardando Avaliação'} + {/* CORREÇÃO VISUAL: Mostra apenas a nota real do Supabase sem strings estáticas duplicadas */} {item.avaliado ? ( {item.nota} / 20 diff --git a/app/Empresas/detalhesAluno.tsx b/app/Empresas/detalhesAluno.tsx index 50b9784..14db71d 100644 --- a/app/Empresas/detalhesAluno.tsx +++ b/app/Empresas/detalhesAluno.tsx @@ -2,20 +2,20 @@ import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import * as WebBrowser from 'expo-web-browser'; import { useCallback, useMemo, useState } from 'react'; import { - ActivityIndicator, - Alert, - RefreshControl, - SafeAreaView, - ScrollView, - StatusBar, - StyleSheet, - Text, - TouchableOpacity, - View + ActivityIndicator, + Alert, + RefreshControl, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; +// Corrigido para usar a biblioteca recomendada e evitar o Warning +import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; @@ -46,21 +46,35 @@ export default function DetalhesAlunoEmpresa() { if (!isManualRefresh) setLoading(true); try { - // 1. Buscar dados do Estágio e do Aluno (Removido o campo cargo) + // 1. Buscar dados do Estágio, Aluno e adicionada a relação com a tabela avaliacoes_empresa const { data, error } = await supabase .from('estagios') .select(` id, data_inicio, data_fim, horas_totais, horas_concluidas, - nota_final, avaliacao_url, - alunos (id, nome, turma_curso, n_escola, profile_id, ano) + estado, + alunos (id, nome, turma_curso, n_escola, profile_id, ano), + avaliacoes_empresa (nota_final) `) .eq('id', estagio_id) - .single(); + .maybeSingle(); // Alterado para maybeSingle para evitar quebras caso não encontre if (error) throw error; + if (!data) { + Alert.alert('Aviso', 'Estágio não encontrado.'); + router.back(); + return; + } + const alunoData = Array.isArray(data.alunos) ? data.alunos[0] : data.alunos; + // Extrair nota final da tabela avaliacoes_empresa (pode vir como objeto único ou array) + const avaliacaoData = Array.isArray(data.avaliacoes_empresa) + ? data.avaliacoes_empresa[0] + : data.avaliacoes_empresa; + + const notaReal = avaliacaoData?.nota_final ?? null; + // 2. Buscar detalhes no Profile (Schema: residencia, data_nascimento, telefone, email) let infoExtra = { telefone: 'N/A', email: 'N/A', residencia: 'N/A', d_nasc: 'N/A' }; @@ -81,7 +95,8 @@ export default function DetalhesAlunoEmpresa() { } } - setEstagio({ ...data, aluno: alunoData, infoExtra }); + // Adicionamos a nota_real ao nosso estado local do estágio + setEstagio({ ...data, aluno: alunoData, infoExtra, nota_real: notaReal }); } catch (error) { console.error(error); @@ -110,8 +125,11 @@ export default function DetalhesAlunoEmpresa() { const progresso = estagio?.horas_totais > 0 ? (estagio.horas_concluidas / estagio.horas_totais) * 100 : 0; + // Verifica se a nota real existe na tabela avaliacoes_empresa + const temNota = estagio?.nota_real !== undefined && estagio?.nota_real !== null; + return ( - + @@ -122,6 +140,7 @@ export default function DetalhesAlunoEmpresa() { { setRefreshing(true); fetchDetalhes(true); }} />} > {/* CABEÇALHO DE IDENTIFICAÇÃO */} @@ -191,25 +210,47 @@ export default function DetalhesAlunoEmpresa() { - {/* AVALIAÇÃO */} + {/* AVALIAÇÃO CORRIGIDA AUTOMÁTICA */} Documentação Oficial - {estagio.nota_final ? ( - WebBrowser.openBrowserAsync(estagio.avaliacao_url)} - > - - Estagiário Avaliado - Classificação: {estagio.nota_final} Valores + {temNota ? ( + + + + Estagiário Avaliado + {/* Agora exibe dinamicamente a nota inserida pela empresa */} + Classificação: {estagio.nota_real} Valores + + - - + + {/* Mantemos o botão de atualizar/editar a avaliação caso queiram alterar alguma coisa */} + router.push({ + pathname: '/Empresas/fichaAvaliacao', + params: { + estagio_id: estagio.id, + aluno_nome: estagio.aluno?.nome, + aluno_turma: estagio.aluno?.turma_curso, + n_escola: estagio.aluno?.n_escola + } + })} + > + + Editar / Consultar Avaliação + + ) : ( router.push({ pathname: '/Empresas/fichaAvaliacao', - params: { estagio_id: estagio.id, aluno_nome: estagio.aluno?.nome } + params: { + estagio_id: estagio.id, + aluno_nome: estagio.aluno?.nome, + aluno_turma: estagio.aluno?.turma_curso, + n_escola: estagio.aluno?.n_escola + } })} > @@ -244,9 +285,9 @@ const styles = StyleSheet.create({ progressoHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, barBg: { height: 10, backgroundColor: '#E2E8F0', borderRadius: 5, overflow: 'hidden' }, barFill: { height: '100%', borderRadius: 5 }, - cardAvaliado: { padding: 20, borderRadius: 20, flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, + cardAvaliado: { padding: 20, borderRadius: 20, flexDirection: 'row', alignItems: 'center' }, textoBranco: { color: '#FFF', fontSize: 14, fontWeight: '600' }, - notaTexto: { color: '#FFF', fontSize: 18, fontWeight: '900' }, + notaTexto: { color: '#FFF', fontSize: 18, fontWeight: '900', marginTop: 2 }, btnAvaliar: { padding: 18, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 10 }, btnTexto: { color: '#FFF', fontSize: 16, fontWeight: '900' } }); \ No newline at end of file diff --git a/app/Empresas/fichaAvaliacao.tsx b/app/Empresas/fichaAvaliacao.tsx index 54e612f..630b0ca 100644 --- a/app/Empresas/fichaAvaliacao.tsx +++ b/app/Empresas/fichaAvaliacao.tsx @@ -1,15 +1,18 @@ -// app/Empresa/FichaAvaliacao.tsx +// app/Empresas/fichaAvaliacao.tsx // Navegar para esta página passando: { estagio_id, aluno_nome, aluno_turma, n_escola } import { Ionicons } from '@expo/vector-icons'; +import { Asset } from 'expo-asset'; +import * as FileSystem from 'expo-file-system/legacy'; +import * as Print from 'expo-print'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { useMemo, useState } from 'react'; +import * as Sharing from 'expo-sharing'; +import { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, KeyboardAvoidingView, Platform, - SafeAreaView, ScrollView, StatusBar, StyleSheet, @@ -18,6 +21,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; @@ -43,7 +47,22 @@ const ESCALA = [ { valor: 5, label: 'Excelente' }, ]; -// ─── Componente ─────────────────────────────────────────────────────────────── +// ─── Helper: imagem em base64 (para incluir no PDF) ──────────────────────────── +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 `data:image/png;base64,${base64.replace(/(\r\n|\n|\r)/gm, '')}`; + } catch (e) { + console.error('Erro no Base64:', e); + return ''; + } +}; + +// ─── Componente Principal ────────────────────────────────────────────────────── export default function FichaAvaliacao() { const { isDarkMode } = useTheme(); const router = useRouter(); @@ -54,10 +73,17 @@ export default function FichaAvaliacao() { n_escola: string; }>(); + // Estados locais dinâmicos para contornar o envio incorreto de UUIDs por parâmetro + const [alunoNome, setAlunoNome] = useState(params.aluno_nome || 'Aluno'); + const [alunoTurma, setAlunoTurma] = useState(params.aluno_turma || '—'); + const [numEscola, setNumEscola] = useState(params.n_escola || '—'); + const [respostas, setRespostas] = useState>({}); const [notaFinal, setNotaFinal] = useState(''); const [observacoes, setObservacoes] = useState(''); const [submitting, setSubmitting] = useState(false); + const [gerandoPDF, setGerandoPDF] = useState(false); + const [loadingDados, setLoadingDados] = useState(true); const cores = useMemo(() => ({ fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', @@ -73,7 +99,69 @@ export default function FichaAvaliacao() { inputFundo: isDarkMode ? '#1E1E20' : '#F8FAFC', }), [isDarkMode]); - // Cor do botão de escala consoante o valor selecionado + // ─── Carregar dados existentes ao entrar na página com Correção de Fallback ─── + useEffect(() => { + async function carregarDadosCompletos() { + if (!params.estagio_id) return; + + try { + // 1. Procurar se já existe avaliação gravada + const { data: avaliacao, error: errAvaliacao } = await supabase + .from('avaliacoes_empresa') + .select('respostas, nota_final, observacoes, aluno_nome, aluno_turma, aluno_n_escola') + .eq('estagio_id', params.estagio_id) + .maybeSingle(); + + if (errAvaliacao) throw errAvaliacao; + + if (avaliacao) { + if (avaliacao.respostas) setRespostas(avaliacao.respostas as Record); + + if (avaliacao.nota_final !== undefined && avaliacao.nota_final !== null) { + const notaNum = Number(avaliacao.nota_final); + setNotaFinal(isNaN(notaNum) ? '' : notaNum.toString()); + } + if (avaliacao.observacoes) setObservacoes(avaliacao.observacoes); + + if (avaliacao.aluno_nome) setAlunoNome(avaliacao.aluno_nome); + if (avaliacao.aluno_turma) setAlunoTurma(avaliacao.aluno_turma); + if (avaliacao.aluno_n_escola) setNumEscola(avaliacao.aluno_n_escola.toString()); + } + + // 2. Fallback robusto olhando o ERD do Supabase: busca real do 'n_escola' e 'turma_curso' + // Executado se o parâmetro recebido estiver em falta ou for detetado um UUID longo (>15 caracteres) + if (!params.aluno_turma || !params.n_escola || params.n_escola.length > 15) { + const { data: estagio, error: errEstagio } = await supabase + .from('estagios') + .select(` + alunos ( + nome, + turma_curso, + n_escola + ) + `) + .eq('id', params.estagio_id) + .maybeSingle(); + + if (!errEstagio && estagio?.alunos) { + const al = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos; + if (al) { + if (al.nome) setAlunoNome(al.nome); + if (al.turma_curso) setAlunoTurma(al.turma_curso); + if (al.n_escola) setNumEscola(al.n_escola.toString()); + } + } + } + } catch (e) { + console.error('Erro ao carregar dados da avaliação:', e); + } finally { + setLoadingDados(false); + } + } + + carregarDadosCompletos(); + }, [params.estagio_id, params.n_escola, params.aluno_turma]); + const corEscala = (valor: number, selecionado: boolean) => { if (!selecionado) return { bg: cores.inputFundo, borda: cores.borda, texto: cores.textoSecundario }; const mapa: Record = { @@ -86,8 +174,8 @@ export default function FichaAvaliacao() { return mapa[valor]; }; - // Validação da nota final const notaValida = useMemo(() => { + if (!notaFinal.trim()) return false; const n = parseFloat(notaFinal.replace(',', '.')); return !isNaN(n) && n >= 0 && n <= 20; }, [notaFinal]); @@ -95,7 +183,6 @@ export default function FichaAvaliacao() { const todasRespondidas = PERGUNTAS.every(p => respostas[p.id] !== undefined); const podeSometer = todasRespondidas && notaValida; - // Agrupa perguntas por categoria const categorias = useMemo(() => { const mapa: Record = {}; PERGUNTAS.forEach(p => { @@ -105,6 +192,7 @@ export default function FichaAvaliacao() { return mapa; }, []); + // ─── Submeter Avaliação ────────────────────────────────────────────────────── const handleSubmit = async () => { if (!podeSometer) { Alert.alert('Atenção', 'Preenche todas as questões e insere uma nota final válida (0–20).'); @@ -112,8 +200,8 @@ export default function FichaAvaliacao() { } Alert.alert( - 'Submeter Avaliação', - `Confirmas a avaliação de ${params.aluno_nome} com nota final ${notaFinal} valores?`, + 'Salvar Avaliação', + `Confirmas a avaliação de ${alunoNome} com nota final de ${notaFinal} valores?`, [ { text: 'Cancelar', style: 'cancel' }, { @@ -123,27 +211,34 @@ export default function FichaAvaliacao() { try { const notaNum = parseFloat(notaFinal.replace(',', '.')); - // Guarda as respostas individuais + nota + observações const { error } = await supabase - .from('estagios') - .update({ + .from('avaliacoes_empresa') + .upsert({ + estagio_id: params.estagio_id, + aluno_nome: alunoNome, + aluno_n_escola: numEscola && numEscola !== '—' ? parseInt(numEscola, 10) : null, + aluno_turma: alunoTurma, nota_final: notaNum, - avaliacao_observacoes: observacoes.trim() || null, - avaliacao_respostas: respostas, // jsonb no Supabase - avaliacao_data: new Date().toISOString(), - }) - .eq('id', params.estagio_id); + respostas: respostas, + observacoes: observacoes.trim() || null, + atualizado_em: new Date().toISOString(), + }, { onConflict: 'estagio_id' }); if (error) throw error; + await supabase + .from('estagios') + .update({ estado: 'Avaliado' }) + .eq('id', params.estagio_id); + Alert.alert( - 'Avaliação Submetida!', - 'A ficha foi gravada com sucesso. O professor será notificado.', + 'Avaliação Guardada!', + 'A ficha foi gravada com sucesso. O professor poderá agora consultá-la.', [{ text: 'OK', onPress: () => router.back() }] ); } catch (e) { console.error(e); - Alert.alert('Erro', 'Não foi possível submeter a avaliação. Tenta novamente.'); + Alert.alert('Erro', 'Não foi possível guardar a avaliação. Tenta novamente.'); } finally { setSubmitting(false); } @@ -153,12 +248,149 @@ export default function FichaAvaliacao() { ); }; - // ─── Render ──────────────────────────────────────────────────────────────── + // ─── Gerar PDF da Avaliação ────────────────────────────────────────────────── + const gerarPDFAvaliacao = async () => { + if (!podeSometer) { + Alert.alert('Atenção', 'Preenche todas as questões e insere uma nota válida (0–20) antes de gerar o PDF.'); + return; + } + setGerandoPDF(true); + try { + const b64Escola = await getBase64Image(require('../../assets/images/logoepvc3.png')); + const b64App = await getBase64Image(require('../../assets/images/logo_s/texto.png')); + const b64Final = await getBase64Image(require('../../assets/images/logoepvc.png')); + + let linhasPerguntas = ''; + Object.entries(categorias).forEach(([categoria, perguntas]) => { + linhasPerguntas += `${categoria}`; + perguntas.forEach((p) => { + const valor = respostas[p.id]; + const label = ESCALA.find(e => e.valor === valor)?.label ?? '—'; + linhasPerguntas += ` + + ${p.texto} + ${valor} · ${label} + `; + }); + }); + + const notaNum = parseFloat(notaFinal.replace(',', '.')); + const positiva = notaNum >= 9.5; + + const html = ` + + + + + + + +
+ + + + + + + +
+

Ficha de Personalidade e Avaliação

+

Formação em Contexto de Trabalho (FCT)

+
+ + + + + +
Estagiário(a):${alunoNome}
Nº Escola / Turma:${numEscola} • ${alunoTurma}
Data de Emissão:${new Date().toLocaleDateString('pt-PT')}
+ +
Avaliação Qualitativa (1 = Insatisfatório • 5 = Excelente)
+ ${linhasPerguntas}
+ +
Classificação Final
+
+
${notaFinal}
+
Valores • ${positiva ? 'APROVADO' : 'REPROVADO'}
+
+ +
Observações da Entidade Acolhedora
+
${observacoes.trim() ? observacoes.replace(/\n/g, '
') : 'Sem observações adicionais registadas.'}
+ + + + `; + + const { uri } = await Print.printToFileAsync({ html }); + const safeName = (alunoNome || 'aluno').replace(/[^a-zA-Z0-9]/g, '_'); + const newUri = `${FileSystem.documentDirectory}Avaliacao_Empresa_${safeName}.pdf`; + await FileSystem.moveAsync({ from: uri, to: newUri }); + if (await Sharing.isAvailableAsync()) { + await Sharing.shareAsync(newUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' }); + } + } catch (e) { + console.error(e); + Alert.alert('Erro', 'Não foi possível gerar o PDF da avaliação.'); + } finally { + setGerandoPDF(false); + } + }; + + if (loadingDados) { + return ( + + + A carregar avaliação existente... + + ); + } + return ( - + - {/* Header */} router.back()} style={s.btnBack}> @@ -172,21 +404,19 @@ export default function FichaAvaliacao() { - {/* Cartão do Aluno */} - {params.aluno_nome?.charAt(0) ?? '?'} + {alunoNome?.charAt(0) ?? '?'} - {params.aluno_nome} - Nº {params.n_escola} · {params.aluno_turma} + {alunoNome} + Nº {numEscola} · {alunoTurma} FCT - {/* Instruções */} @@ -194,21 +424,18 @@ export default function FichaAvaliacao() { - {/* Perguntas agrupadas por categoria */} {Object.entries(categorias).map(([categoria, perguntas], catIdx) => ( - {/* Cabeçalho de categoria */} {categoria} - {perguntas.map((pergunta, idx) => { + {perguntas.map((pergunta) => { const selecionado = respostas[pergunta.id]; const globalIdx = PERGUNTAS.findIndex(p => p.id === pergunta.id) + 1; return ( - {/* Número + Texto */} {globalIdx} @@ -216,7 +443,6 @@ export default function FichaAvaliacao() { {pergunta.texto} - {/* Escala */} {ESCALA.map(e => { const esteSelecionado = selecionado === e.valor; @@ -234,7 +460,6 @@ export default function FichaAvaliacao() { })} - {/* Label do selecionado */} {selecionado && ( {ESCALA.find(e => e.valor === selecionado)?.label} @@ -246,11 +471,10 @@ export default function FichaAvaliacao() { ))} - {/* Nota Final */} Nota Final - Atribui uma nota de 0 a 20 valores. Abaixo de 9,5 é negativa. + Insira a nota obtida na escala de 0 a 20 valores. @@ -263,14 +487,14 @@ export default function FichaAvaliacao() { color: cores.texto, }, ]} - placeholder="Ex: 16" + placeholder="0-20" placeholderTextColor={cores.textoSecundario} keyboardType="decimal-pad" value={notaFinal} onChangeText={setNotaFinal} maxLength={4} /> - / 20 valores + valores {notaFinal !== '' && ( - {/* Observações */} Observações Opcional — comentários adicionais sobre o desempenho. @@ -306,7 +529,6 @@ export default function FichaAvaliacao() { /> - {/* Progresso */} @@ -327,27 +549,43 @@ export default function FichaAvaliacao() { - {/* Botão Submeter */} - - {submitting ? ( - - ) : ( - <> - - - Submeter Avaliação - - - )} - + + + {submitting ? ( + + ) : ( + <> + + + Salvar Avaliação + + + )} + + + + {gerandoPDF ? ( + + ) : ( + <> + + + Exportar PDF + + + )} + + @@ -356,7 +594,7 @@ export default function FichaAvaliacao() { ); } -// ─── Estilos ────────────────────────────────────────────────────────────────── +// ─── Estilos Customizados ────────────────────────────────────────────────────── const s = StyleSheet.create({ safe: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingTop: 15, paddingBottom: 12, gap: 14 }, @@ -365,7 +603,9 @@ const s = StyleSheet.create({ headerSub: { fontSize: 12, fontWeight: '500', marginTop: 1 }, scroll: { paddingHorizontal: 16, paddingBottom: 20 }, - // Aluno card + loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }, + loadingText: { fontSize: 14, fontWeight: '600' }, + alunoCard: { flexDirection: 'row', alignItems: 'center', borderRadius: 18, padding: 18, marginBottom: 16 }, alunoAvatar: { width: 46, height: 46, borderRadius: 23, backgroundColor: 'rgba(255,255,255,0.2)', justifyContent: 'center', alignItems: 'center' }, alunoAvatarLetra: { color: '#fff', fontSize: 20, fontWeight: '900' }, @@ -374,48 +614,42 @@ const s = StyleSheet.create({ badgeFCT: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8 }, badgeFCTText: { color: '#fff', fontSize: 11, fontWeight: '900', letterSpacing: 1 }, - // Instruções instrucoes: { flexDirection: 'row', alignItems: 'flex-start', borderRadius: 12, borderWidth: 1, padding: 12, marginBottom: 20 }, instrucoesTxt: { flex: 1, fontSize: 13, lineHeight: 19 }, - // Categoria catHeader: { borderRadius: 8, paddingVertical: 7, paddingHorizontal: 14, marginBottom: 10, marginTop: 6 }, catHeaderTxt: { color: '#fff', fontSize: 11, fontWeight: '800', letterSpacing: 1, textTransform: 'uppercase' }, - // Pergunta perguntaCard: { borderRadius: 16, borderWidth: 1, padding: 16, marginBottom: 10 }, perguntaTop: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 14, gap: 12 }, numCircle: { width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', flexShrink: 0, marginTop: 1 }, numTxt: { fontSize: 13, fontWeight: '900' }, perguntaTxt: { flex: 1, fontSize: 14, fontWeight: '600', lineHeight: 20 }, - // Escala escalaRow: { flexDirection: 'row', gap: 8, justifyContent: 'space-between' }, escalaBotao: { flex: 1, aspectRatio: 1, borderRadius: 12, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center', maxWidth: 54 }, escalaNum: { fontSize: 17, fontWeight: '900' }, escalaLabel: { fontSize: 11, fontWeight: '600', marginTop: 8, textAlign: 'center' }, - // Nota final secaoFinal: { borderRadius: 18, borderWidth: 1, padding: 18, marginBottom: 14 }, secaoTitulo: { fontSize: 16, fontWeight: '900', marginBottom: 4 }, secaoDesc: { fontSize: 13, marginBottom: 14, lineHeight: 18 }, notaRow: { flexDirection: 'row', alignItems: 'center', gap: 12 }, - notaInput: { borderWidth: 2, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, fontSize: 22, fontWeight: '900', width: 90, textAlign: 'center' }, + notaInput: { borderWidth: 2, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, fontSize: 22, fontWeight: '900', width: 95, textAlign: 'center' }, notaSufixo: { fontSize: 15, fontWeight: '700' }, notaBadge: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8 }, notaBadgeTxt: { fontSize: 13, fontWeight: '800' }, - // Observações obsInput: { borderWidth: 1.5, borderRadius: 12, padding: 14, fontSize: 14, minHeight: 100, lineHeight: 21 }, - // Progresso progressoBox: { borderRadius: 14, borderWidth: 1, padding: 14, marginBottom: 16 }, progressoRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 }, progressoLabel: { fontSize: 12, fontWeight: '600' }, progressoBar: { height: 6, borderRadius: 3, overflow: 'hidden' }, progressoFill: { height: '100%', borderRadius: 3 }, - // Botão submeter - btnSubmeter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, paddingVertical: 17, borderRadius: 18, marginTop: 4 }, - btnSubmeterTxt: { fontSize: 16, fontWeight: '900' }, + botoesRow: { flexDirection: 'row', gap: 10, marginTop: 4 }, + btnSalvar: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 16, borderRadius: 18 }, + btnPDF: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 16, borderRadius: 18 }, + btnTxt: { fontSize: 14, fontWeight: '900' }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/relatorios.tsx b/app/Professor/Alunos/relatorios.tsx index 3ab4f78..14c0870 100644 --- a/app/Professor/Alunos/relatorios.tsx +++ b/app/Professor/Alunos/relatorios.tsx @@ -1,4 +1,3 @@ -// app/Professor/Alunos/relatorios.tsx import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { Asset } from 'expo-asset'; @@ -6,7 +5,6 @@ import * as FileSystem from 'expo-file-system/legacy'; import * as Print from 'expo-print'; import { useRouter } from 'expo-router'; import * as Sharing from 'expo-sharing'; -import * as WebBrowser from 'expo-web-browser'; import { useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, @@ -24,6 +22,28 @@ import { import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; +// ─── Perguntas/Escala (idênticas às usadas pela empresa) ────────────────────── +const PERGUNTAS_EMP = [ + { id: 'p1', categoria: 'Competências Técnicas', texto: 'Domínio das tarefas e conhecimentos técnicos exigidos.' }, + { id: 'p2', categoria: 'Competências Técnicas', texto: 'Capacidade de aprendizagem e adaptação a novas situações.' }, + { id: 'p3', categoria: 'Competências Técnicas', texto: 'Qualidade e rigor do trabalho realizado.' }, + { id: 'p4', categoria: 'Atitude Profissional', texto: 'Pontualidade, assiduidade e cumprimento de horários.' }, + { id: 'p5', categoria: 'Atitude Profissional', texto: 'Iniciativa, proatividade e autonomia nas tarefas.' }, + { id: 'p6', categoria: 'Atitude Profissional', texto: 'Responsabilidade e cumprimento das normas da empresa.' }, + { id: 'p7', categoria: 'Relacionamento', texto: 'Relacionamento com colegas e integração na equipa.' }, + { id: 'p8', categoria: 'Relacionamento', texto: 'Comunicação e postura com clientes e superiores.' }, + { id: 'p9', categoria: 'Desenvolvimento Pessoal', texto: 'Capacidade de gerir dificuldades e resolver problemas.' }, + { id: 'p10', categoria: 'Desenvolvimento Pessoal', texto: 'Evolução e progresso ao longo do período de estágio.' }, +]; + +const ESCALA_EMP = [ + { valor: 1, label: 'Insatisfatório' }, + { valor: 2, label: 'A Melhorar' }, + { valor: 3, label: 'Satisfatório' }, + { valor: 4, label: 'Bom' }, + { valor: 5, label: 'Excelente' }, +]; + export default function GestaoRelatorios() { const { isDarkMode } = useTheme(); const router = useRouter(); @@ -56,7 +76,7 @@ export default function GestaoRelatorios() { return `data:image/png;base64,${base64.replace(/(\r\n|\n|\r)/gm, "")}`; } catch (e) { console.error("Erro no Base64:", e); - return ""; + return ""; } }; @@ -64,7 +84,7 @@ export default function GestaoRelatorios() { 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 { @@ -72,9 +92,10 @@ export default function GestaoRelatorios() { .from('estagios') .select(` id, horas_totais, horas_concluidas, horas_diarias, nota_final, avaliacao_url, data_inicio, data_fim, - alunos (id, nome, turma_curso, n_escola, ano), + alunos (id, nome, turma_curso, n_escola, ano), empresas (nome, tutor_nome), - autoavaliacoes (id, assiduidade, responsabilidade, relacionamento, conhecimentos, nota_desejada, comentarios, criado_em) + autoavaliacoes (id, assiduidade, responsabilidade, relacionamento, conhecimentos, nota_desejada, comentarios, criado_em), + avaliacoes_empresa (respostas, observacoes, atualizado_em) `) .order('data_inicio', { ascending: false }); @@ -84,7 +105,8 @@ 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 autoavaliacao = Array.isArray(estagio.autoavaliacoes) ? estagio.autoavaliacoes[0] : estagio.autoavaliacoes; - + const avaliacaoEmp = Array.isArray(estagio.avaliacoes_empresa) ? estagio.avaliacoes_empresa[0] : estagio.avaliacoes_empresa; + const nomeCursoFormatado = formatarTexto(aluno?.turma_curso); const anoAluno = aluno?.ano || 10; const turmaCompleta = `${anoAluno}º ${nomeCursoFormatado}`; @@ -95,7 +117,7 @@ export default function GestaoRelatorios() { aluno_nome: aluno?.nome || 'Desconhecido', curso: nomeCursoFormatado, ano: anoAluno, - turma: turmaCompleta, + turma: turmaCompleta, n_escola: aluno?.n_escola || '--', empresa_nome: empresa?.nome || 'N/A', tutor_nome: empresa?.tutor_nome || 'N/A', @@ -105,8 +127,8 @@ export default function GestaoRelatorios() { horas_totais: estagio.horas_totais || 0, horas_concluidas: estagio.horas_concluidas || 0, nota_empresa: estagio.nota_final, - pdf_empresa: estagio.avaliacao_url, - autoavaliacao: autoavaliacao || null + autoavaliacao: autoavaliacao || null, + avaliacao_empresa_detalhes: avaliacaoEmp || null }; }) || []; @@ -124,16 +146,12 @@ export default function GestaoRelatorios() { const cursosAgrupados = useMemo(() => { const grupos: Record = {}; - relatorios.forEach(r => { - if (!grupos[r.curso]) { - grupos[r.curso] = { count10: 0, count11: 0, count12: 0 }; - } + 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]); @@ -142,45 +160,52 @@ export default function GestaoRelatorios() { return relatorios.filter(r => r.turma === turmaAtiva); }, [relatorios, turmaAtiva]); - // ===================================================================== - // GERAÇÃO DO CABEÇALHO E RODAPÉ COM OS SÍMBOLOS E MARCA DE ÁGUA - // ===================================================================== + // ─── Cabeçalho/Rodapé/Marca de água partilhados ──────────────────────────── const gerarCabecalhoERodape = async () => { - const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc3.png')); - const b64App = await getBase64Image(require('../../../assets/images/logo_s/texto.png')); - // A imagem que tem os vários logótipos em baixo - const b64LogosFinais = await getBase64Image(require('../../../assets/images/logoepvc.png')); +const logoEscola = require('../../assets/logoepvc3.png'); +const logoEstagiosPlus = require('../../assets/logo_estagios_plus.png'); +const logoRodape = require('../../assets/logoepvc.png'); - return { - header: ` - - - - - - -
- - - - -
- `, - footer: ` -
- -
- Documento gerado digitalmente via plataforma Estágios+ EPVC • ${new Date().toLocaleDateString('pt-PT')} -
-
- `, - watermark: ` -
- -
- ` - }; - }; + const header = ` +
+ + + + + + +
+ + + + +
Plataforma Oficial
+ +
+
+ `; + + const footer = ` +
+ + + + + +
Documento gerado automaticamente pela Plataforma Estágios+ + +
+
+ `; + + const watermark = ` +
+ +
+ `; + + return { header, footer, watermark }; +}; const getEstilosHTML = () => ` `; - // ===================================================================== - // 1 & 2. GERAÇÃO DA MATRIZ EM HTML (USADA NO EXCEL E NO PDF HORIZONTAL) - // ===================================================================== + // ─── Matriz de Assiduidade (mantida) ─────────────────────────────────────── const construirHtmlMatriz = async () => { const { header, footer } = await gerarCabecalhoERodape(); const tituloMatriz = `

Mapa Oficial de Assiduidade

Formação em Contexto de Trabalho

`; @@ -215,7 +238,7 @@ export default function GestaoRelatorios() { 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(); @@ -229,8 +252,7 @@ export default function GestaoRelatorios() { 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) { + 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); } @@ -242,14 +264,11 @@ export default function GestaoRelatorios() { let mesesHtml = ` Nº Nome do Aluno`; - let diasHtml = ` - `; + let diasHtml = ``; meses.forEach((m: any) => { mesesHtml += `${m.nome}`; - m.dias.forEach((d: any) => { - diasHtml += `${d.dia}`; - }); + m.dias.forEach((d: any) => { diasHtml += `${d.dia}`; }); diasHtml += `Total`; }); @@ -263,13 +282,12 @@ export default function GestaoRelatorios() { alunosHtml += ` ${r.n_escola} ${r.aluno_nome}`; - + 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) { @@ -288,11 +306,10 @@ export default function GestaoRelatorios() { alunosHtml += ``; } }); - totalFeitasGlobais += totalMesFeitas; alunosHtml += `${totalMesFeitas}`; }); - + const faltam = Math.max(0, (r.horas_totais || 400) - totalFeitasGlobais); alunosHtml += ` ${totalFeitasGlobais} @@ -311,11 +328,7 @@ export default function GestaoRelatorios() { Ano Letivo: 2025/2026 - - ${mesesHtml} - ${diasHtml} - ${alunosHtml} -
+ ${mesesHtml}${diasHtml}${alunosHtml}
Legenda: [ F ] Falta Justificada • [ FI ] Falta Injustificada
@@ -326,39 +339,21 @@ export default function GestaoRelatorios() { const gerarExcelGeral = async () => { if (relatoriosFiltrados.length === 0) return Alert.alert("Aviso", "Não há alunos para exportar nesta turma."); - setGerandoDocumento("excel"); + setGerandoDocumento("excel"); try { const htmlCorpo = await construirHtmlMatriz(); const docExcel = ` - - - ${getEstilosHTML()} - - - ${htmlCorpo} - - - `; - + ${getEstilosHTML()} + ${htmlCorpo}`; const fileNameTurma = turmaAtiva ? turmaAtiva.replace(/[^a-zA-Z0-9]/g, '') : 'Geral'; const fileUri = FileSystem.documentDirectory + `Mapa_Assiduidade_${fileNameTurma}.xls`; - await FileSystem.writeAsStringAsync(fileUri, docExcel, { encoding: FileSystem.EncodingType.UTF8 }); - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(fileUri, { - mimeType: 'application/vnd.ms-excel', - dialogTitle: 'Exportar Matriz Excel', - UTI: 'com.microsoft.excel.xls' - }); + await Sharing.shareAsync(fileUri, { mimeType: 'application/vnd.ms-excel', dialogTitle: 'Exportar Matriz Excel', UTI: 'com.microsoft.excel.xls' }); } - } catch (e) { - console.error(e); - Alert.alert('Erro', 'Falha ao exportar a Matriz Excel.'); - } finally { - setGerandoDocumento(null); - } + } catch (e) { console.error(e); Alert.alert('Erro', 'Falha ao exportar a Matriz Excel.'); } + finally { setGerandoDocumento(null); } }; const gerarPautaPDF = async () => { @@ -366,147 +361,201 @@ export default function GestaoRelatorios() { setGerandoDocumento("pdf"); try { const htmlCorpo = await construirHtmlMatriz(); - - const htmlCompletoPDF = ` - - - - - ${getEstilosHTML()} - - - - ${htmlCorpo} - - - `; - + const htmlCompletoPDF = `${getEstilosHTML()} + + ${htmlCorpo}`; const { uri } = await Print.printToFileAsync({ html: htmlCompletoPDF }); const fileNameTurma = turmaAtiva ? turmaAtiva.replace(/[^a-zA-Z0-9]/g, '') : 'Geral'; const newFileUri = `${FileSystem.documentDirectory}Mapa_Assiduidade_${fileNameTurma}.pdf`; - await FileSystem.moveAsync({ from: uri, to: newFileUri }); - - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' }); - } - } catch (e) { - console.error(e); - Alert.alert('Erro', 'Falha ao gerar o PDF Oficial.'); - } finally { - setGerandoDocumento(null); - } + if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' }); + } catch (e) { console.error(e); Alert.alert('Erro', 'Falha ao gerar o PDF Oficial.'); } + finally { setGerandoDocumento(null); } }; - // ===================================================================== - // 3. EXPORTAR A AUTOAVALIAÇÃO DO ALUNO - // ===================================================================== + // ─── PDF da Autoavaliação (mantido) ──────────────────────────────────────── const gerarAutoavaliacaoPDF = async (r: any) => { if (!r.autoavaliacao) return Alert.alert("Aviso", "Este aluno ainda não submeteu a sua autoavaliação."); - setGerandoDocumento(`auto_${r.id_estagio}`); try { const { header, footer, watermark } = await gerarCabecalhoERodape(); const tituloAuto = `

Ficha de Autoavaliação

Formação em Contexto de Trabalho

`; const headerCompleto = header.replace('', tituloAuto); - const av = r.autoavaliacao; - - const htmlContent = ` - - - - - - - ${watermark} - ${headerCompleto} - - - - - - -
Estagiário(a):${r.aluno_nome}
Turma e Curso:${r.turma}
Entidade de Acolhimento:${r.empresa_nome}
Data de Submissão:${new Date(av.criado_em).toLocaleDateString('pt-PT')}
- -
-
-
Critérios Qualitativos
- - - - - - -
Domínio AvaliadoEscala (1 a 5)
Assiduidade e Pontualidade${av.assiduidade}
Responsabilidade e Iniciativa${av.responsabilidade}
Relacionamento Interpessoal${av.relacionamento}
Aplicação de Conhecimentos Práticos${av.conhecimentos}
-
- -
-
Nota Final
-
-
${av.nota_desejada}
-
Valores
-
-
+ const htmlContent = ` + + ${watermark}${headerCompleto} + + + + + +
Estagiário(a):${r.aluno_nome}
Turma e Curso:${r.turma}
Entidade de Acolhimento:${r.empresa_nome}
Data de Submissão:${new Date(av.criado_em).toLocaleDateString('pt-PT')}
+
+
+
Critérios Qualitativos
+ + + + + + +
Domínio AvaliadoEscala (1 a 5)
Assiduidade e Pontualidade${av.assiduidade}
Responsabilidade e Iniciativa${av.responsabilidade}
Relacionamento Interpessoal${av.relacionamento}
Aplicação de Conhecimentos Práticos${av.conhecimentos}
- -
Justificação e Comentários do Aluno
-
- ${av.comentarios ? av.comentarios.replace(/\n/g, '
') : 'O aluno não adicionou comentários ou justificação à sua nota.'} +
+
Nota Final
+
+
${av.nota_desejada}
+
Valores
+
- - ${footer} - - - `; +
+
Justificação e Comentários do Aluno
+
${av.comentarios ? av.comentarios.replace(/\n/g, '
') : 'O aluno não adicionou comentários ou justificação à sua nota.'}
+ ${footer}`; const { uri } = await Print.printToFileAsync({ html: htmlContent }); const safeName = r.aluno_nome.replace(/[^a-zA-Z0-9]/g, '_'); const newFileUri = `${FileSystem.documentDirectory}Autoavaliacao_${safeName}.pdf`; await FileSystem.moveAsync({ from: uri, to: newFileUri }); if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' }); + } catch (e) { console.error(e); Alert.alert('Erro', 'Não foi possível gerar a autoavaliação.'); } + finally { setGerandoDocumento(null); } + }; + + // ─── NOVO: PDF da Avaliação da Empresa ───────────────────────────────────── + + const gerarAvaliacaoEmpresaPDF = async (r: any) => { + if (r.nota_empresa == null && !r.avaliacao_empresa_detalhes) { + Alert.alert('Aviso', 'A empresa ainda não avaliou este aluno.'); + return; + } + + setGerandoDocumento(`emp_${r.id_estagio}`); + try { + const dadosAvaliacao = r.avaliacao_empresa_detalhes; + + let respostas: Record = {}; + if (dadosAvaliacao?.respostas) { + respostas = typeof dadosAvaliacao.respostas === 'string' + ? JSON.parse(dadosAvaliacao.respostas) + : dadosAvaliacao.respostas; + } + + const categorias: Record = {}; + PERGUNTAS_EMP.forEach(p => { + if (!categorias[p.categoria]) categorias[p.categoria] = []; + categorias[p.categoria].push(p); + }); + + let linhasPerguntas = ''; + Object.entries(categorias).forEach(([categoria, perguntas]) => { + linhasPerguntas += `${categoria}`; + perguntas.forEach((p) => { + const valor = respostas[p.id]; + const label = ESCALA_EMP.find(e => e.valor === valor)?.label ?? '—'; + linhasPerguntas += ` + + ${p.texto} + ${valor ?? '—'} · ${label} + `; + }); + }); + + const notaNum = Number(r.nota_empresa); + const positiva = notaNum >= 9.5; + const dataAval = dadosAvaliacao?.atualizado_em ? new Date(dadosAvaliacao.atualizado_em).toLocaleDateString('pt-PT') : '—'; + const observacoes = dadosAvaliacao?.observacoes || ''; + + const { header, footer, watermark } = await gerarCabecalhoERodape(); + const tituloEmp = `

Ficha de Avaliação da Entidade

Formação em Contexto de Trabalho

`; + const headerCompleto = header.replace('', tituloEmp); + + const html = ` + + + ${watermark} + ${headerCompleto} + + + + + + +
Estagiário(a):${r.aluno_nome}
Nº Escola / Turma:${r.n_escola} · ${r.turma}
Entidade de Acolhimento:${r.empresa_nome}
Tutor(a) Responsável:${r.tutor_nome}
Data da Avaliação:${dataAval}
+ +
Avaliação Qualitativa (1 = Insatisfatório · 5 = Excelente)
+ ${linhasPerguntas}
+ +
Nota Final Atribuída pela Entidade
+
+
${notaNum}
+
/ 20 valores · ${positiva ? 'POSITIVA' : 'NEGATIVA'}
+
+ +
Observações da Entidade
+
${observacoes.trim() ? observacoes.replace(/\n/g,'
') : 'Sem observações adicionais.'}
+ + ${footer} + `; + + const { uri } = await Print.printToFileAsync({ html }); + const safeName = r.aluno_nome.replace(/[^a-zA-Z0-9]/g, '_'); + const newFileUri = `${FileSystem.documentDirectory}Avaliacao_Empresa_${safeName}.pdf`; + await FileSystem.moveAsync({ from: uri, to: newFileUri }); + if (await Sharing.isAvailableAsync()) { + await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' }); + } } catch (e) { console.error(e); - Alert.alert('Erro', 'Não foi possível gerar a autoavaliação.'); + Alert.alert('Erro', 'Não foi possível gerar a avaliação da empresa.'); } finally { setGerandoDocumento(null); } }; - // ===================================================================== - // 4. EXPORTAR O DIÁRIO DE BORDO INDIVIDUAL - // ===================================================================== + // ─── Diário de Bordo (mantido) ───────────────────────────────────────────── const gerarSumariosPDF = async (r: any) => { setGerandoDocumento(r.id_estagio); try { 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 (!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 (!sumarios || sumarios.length === 0) { Alert.alert('Aviso', 'Não foram encontrados registos para este aluno.'); - setGerandoDocumento(null); - return; + setGerandoDocumento(null); return; } let linhasTabela = ''; @@ -518,151 +567,90 @@ export default function GestaoRelatorios() { const tituloDiario = `

Diário de Bordo

Formação em Contexto de Trabalho

`; const headerCompleto = header.replace('', tituloDiario); - const htmlContent = ` - - - - - - - ${watermark} - ${headerCompleto} - - - - -
Estagiário:${r.aluno_nome}Turma:${r.turma}
Entidade:${r.empresa_nome}Tutor(a):${r.tutor_nome}
-
Registo de Atividades Diárias
- ${linhasTabela}
DataNaturezaSumário
- - ${footer} - - - `; + const htmlContent = ` + + ${watermark}${headerCompleto} + + + +
Estagiário:${r.aluno_nome}Turma:${r.turma}
Entidade:${r.empresa_nome}Tutor(a):${r.tutor_nome}
+
Registo de Atividades Diárias
+ ${linhasTabela}
DataNaturezaSumário
+ ${footer}`; const { uri } = await Print.printToFileAsync({ html: htmlContent }); 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', UTI: 'com.adobe.pdf' }); - } catch (e) { - console.error(e); - Alert.alert('Erro', 'Não foi possível ler os registos.'); - } finally { - setGerandoDocumento(null); - } + } catch (e) { console.error(e); Alert.alert('Erro', 'Não foi possível ler os registos.'); } + finally { setGerandoDocumento(null); } }; return ( - + - { - if (turmaAtiva) setTurmaAtiva(null); - else router.back(); - }}> + { if (turmaAtiva) setTurmaAtiva(null); else router.back(); }}> - - {turmaAtiva ? turmaAtiva : 'Relatórios por Turma'} - + {turmaAtiva ? turmaAtiva : 'Relatórios por Turma'} - { setRefreshing(true); fetchRelatorios(true); }} tintColor={cores.azulMarinho} />} > - {loading && !refreshing ? ( ) : !turmaAtiva ? ( {cursosAgrupados.map((cursoGrupo, index) => ( - {cursoGrupo.curso} - - 0 ? 1 : 0.5 }]} - activeOpacity={0.7} - disabled={cursoGrupo.count10 === 0} - onPress={() => setTurmaAtiva(`10º ${cursoGrupo.curso}`)} - > + 0 ? 1 : 0.5 }]} activeOpacity={0.7} disabled={cursoGrupo.count10 === 0} onPress={() => setTurmaAtiva(`10º ${cursoGrupo.curso}`)}> 10º {cursoGrupo.count10} Aluno(s) - - 0 ? 1 : 0.5 }]} - activeOpacity={0.7} - disabled={cursoGrupo.count11 === 0} - onPress={() => setTurmaAtiva(`11º ${cursoGrupo.curso}`)} - > + 0 ? 1 : 0.5 }]} activeOpacity={0.7} disabled={cursoGrupo.count11 === 0} onPress={() => setTurmaAtiva(`11º ${cursoGrupo.curso}`)}> 11º {cursoGrupo.count11} Aluno(s) - - 0 ? 1 : 0.5 }]} - activeOpacity={0.7} - disabled={cursoGrupo.count12 === 0} - onPress={() => setTurmaAtiva(`12º ${cursoGrupo.curso}`)} - > + 0 ? 1 : 0.5 }]} activeOpacity={0.7} disabled={cursoGrupo.count12 === 0} onPress={() => setTurmaAtiva(`12º ${cursoGrupo.curso}`)}> 12º {cursoGrupo.count12} Aluno(s) - ))} - {cursosAgrupados.length === 0 && ( - Nenhum estágio em sistema. + Nenhum estágio em sistema. )} ) : ( - - {gerandoDocumento === "excel" ? ( - - ) : ( - <> - - Matriz em Excel - + + {gerandoDocumento === "excel" ? : ( + <>Matriz em Excel )} - - - {gerandoDocumento === "pdf" ? ( - - ) : ( - <> - - Matriz em PDF - + + {gerandoDocumento === "pdf" ? : ( + <>Matriz em PDF )} @@ -680,7 +668,6 @@ export default function GestaoRelatorios() { ) : ( relatoriosFiltrados.map((r, index) => ( - {r.aluno_nome.charAt(0)} @@ -693,23 +680,30 @@ export default function GestaoRelatorios() { - {/* 1. AVALIAÇÃO DA EMPRESA */} + {/* 1. AVALIAÇÃO DA EMPRESA — agora gera o PDF on-demand */} 1. Avaliação da Empresa - {r.nota_empresa ? ( + {r.nota_empresa != null ? ( {r.nota_empresa} Val. ) : ( Pendente )} - {r.pdf_empresa && ( - WebBrowser.openBrowserAsync(r.pdf_empresa)} + {r.nota_empresa != null && ( + gerarAvaliacaoEmpresaPDF(r)} + disabled={gerandoDocumento === `emp_${r.id_estagio}`} > - - Ver Ficha de Avaliação + {gerandoDocumento === `emp_${r.id_estagio}` ? ( + + ) : ( + <> + + Exportar Avaliação (PDF) + + )} )} @@ -725,8 +719,8 @@ export default function GestaoRelatorios() { )} {r.autoavaliacao && ( - gerarAutoavaliacaoPDF(r)} disabled={gerandoDocumento === `auto_${r.id_estagio}`} > @@ -748,8 +742,8 @@ export default function GestaoRelatorios() { 3. Diário de Bordo {r.horas_concluidas}h Registadas - gerarSumariosPDF(r)} disabled={gerandoDocumento === r.id_estagio} > @@ -763,13 +757,11 @@ export default function GestaoRelatorios() { )} - )) )} )} - ); @@ -780,46 +772,46 @@ const styles = StyleSheet.create({ 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: 0 }, btnGlobalText: { fontSize: 13, fontWeight: '900', marginTop: 8 }, - + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', letterSpacing: 1 }, - + 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', letterSpacing: -0.5 }, alunoSub: { fontSize: 12, fontWeight: '600', marginTop: 2 }, divider: { height: 1, marginVertical: 15 }, - + moduloBox: { borderBottomWidth: 1, borderBottomColor: '#E2E8F0', paddingBottom: 15, marginBottom: 15 }, moduloHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, moduloTitle: { fontSize: 13, fontWeight: '800' }, notaTag: { fontSize: 11, fontWeight: '900', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 }, - - btnAcaoLigeiro: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - paddingVertical: 12, - borderRadius: 12, - borderWidth: 1, - marginTop: 12, - gap: 8 + + btnAcaoLigeiro: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + paddingVertical: 12, + borderRadius: 12, + borderWidth: 1, + marginTop: 12, + gap: 8 }, textoAcao: { fontSize: 13, fontWeight: '800' } }); \ No newline at end of file