From f8d12176fa64eb0cf96eddd6f198de60d492ef36 Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Fri, 8 May 2026 10:40:28 +0100 Subject: [PATCH] atualizacoes --- app/Empresas/fichaAvaliacao.tsx | 706 ++++++++++++++++------------ app/Professor/Alunos/relatorios.tsx | 419 +++++++++++++++++ app/Professor/ProfessorHome.tsx | 2 +- 3 files changed, 836 insertions(+), 291 deletions(-) create mode 100644 app/Professor/Alunos/relatorios.tsx diff --git a/app/Empresas/fichaAvaliacao.tsx b/app/Empresas/fichaAvaliacao.tsx index 020b123..debec27 100644 --- a/app/Empresas/fichaAvaliacao.tsx +++ b/app/Empresas/fichaAvaliacao.tsx @@ -1,54 +1,36 @@ -// app/Empresas/fichaAvaliacao.tsx +// 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 { useLocalSearchParams, useRouter } from 'expo-router'; +import { useRouter } from 'expo-router'; import * as Sharing from 'expo-sharing'; -import { useMemo, useState } from 'react'; +import * as WebBrowser from 'expo-web-browser'; +import { useCallback, useMemo, useState } from 'react'; import { - ActivityIndicator, - Alert, - KeyboardAvoidingView, - Platform, - ScrollView, - StatusBar, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - View + ActivityIndicator, + Alert, + RefreshControl, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -export default function FichaAvaliacao() { - const router = useRouter(); +export default function GestaoRelatorios() { const { isDarkMode } = useTheme(); - const params = useLocalSearchParams(); + const router = useRouter(); - const estagio_id = Array.isArray(params.estagio_id) ? params.estagio_id[0] : params.estagio_id; - const aluno_nome = Array.isArray(params.aluno_nome) ? params.aluno_nome[0] : params.aluno_nome; - - const [loading, setLoading] = useState(false); - - // As 10 Perguntas Oficiais das Escolas Profissionais - const [criterios, setCriterios] = useState({ - assiduidade: 0, - relacionamento: 0, - responsabilidade: 0, - iniciativa: 0, - adaptacao: 0, - conhecimentos: 0, - qualidade: 0, - empenho: 0, - equipamentos: 0, - seguranca: 0, - }); - - const [notaFinal, setNotaFinal] = useState(''); - const [observacoes, setObservacoes] = useState(''); + const [relatorios, setRelatorios] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [gerandoPDF, setGerandoPDF] = useState(null); const cores = useMemo(() => ({ fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', @@ -61,6 +43,7 @@ export default function FichaAvaliacao() { laranja: '#F18721', }), [isDarkMode]); + // Função para carregar as imagens locais para Base64 const getBase64Image = async (imageModule: any) => { try { const asset = Asset.fromModule(imageModule); @@ -75,86 +58,259 @@ export default function FichaAvaliacao() { } }; - const ClassificacaoRow = ({ label, field }: { label: string, field: keyof typeof criterios }) => ( - - {label} - - {[1, 2, 3, 4, 5].map((num) => { - const selecionado = criterios[field] === num; - return ( - setCriterios({ ...criterios, [field]: num })} - style={[ - styles.botaoNota, - { - borderColor: selecionado ? cores.azulMarinho : cores.borda, - backgroundColor: selecionado ? cores.azulMarinho : cores.card, - } - ]} - > - - {num} - - - ); - })} - - - ); - - const submeterAvaliacao = async () => { - const faltamRespostas = Object.values(criterios).some(val => val === 0); - if (faltamRespostas) { - Alert.alert('Aviso', 'Preencha todos os 10 parâmetros antes de gerar o PDF.'); - return; - } - - const notaNum = parseInt(notaFinal); - if (isNaN(notaNum) || notaNum < 0 || notaNum > 20) { - Alert.alert('Erro', 'Nota final inválida (0-20).'); - return; - } - - setLoading(true); - + const fetchRelatorios = async (isManualRefresh = false) => { + if (!isManualRefresh) setLoading(true); try { - // 1. DADOS DO SUPABASE: Substituído "cargo" por "horas_totais" no destaque - const { data: infoEstagio, error: infoError } = await supabase + const { data, error } = await supabase .from('estagios') .select(` - data_inicio, - data_fim, - horas_totais, - empresas (nome, tutor_nome), - alunos (nome, n_escola, turma_curso) + id, horas_totais, horas_concluidas, nota_final, avaliacao_url, data_inicio, data_fim, + alunos (id, nome, turma_curso, n_escola), + empresas (nome, tutor_nome) `) - .eq('id', estagio_id) - .single(); + .order('data_inicio', { ascending: false }); - if (infoError) console.warn("Erro a buscar dados:", infoError); + if (error) throw error; - const empresaData = Array.isArray(infoEstagio?.empresas) ? infoEstagio?.empresas[0] : infoEstagio?.empresas; - const alunoData = Array.isArray(infoEstagio?.alunos) ? infoEstagio?.alunos[0] : infoEstagio?.alunos; + const formatados = data?.map((estagio: any) => { + const aluno = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos; + const empresa = Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas; + + return { + id_estagio: estagio.id, + aluno_id: aluno?.id, + aluno_nome: aluno?.nome || 'Desconhecido', + turma: aluno?.turma_curso || 'N/A', + 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_totais: estagio.horas_totais || 0, + horas_concluidas: estagio.horas_concluidas || 0, + nota_empresa: estagio.nota_final, + pdf_empresa: estagio.avaliacao_url + }; + }) || []; - const nomeEmpresa = empresaData?.nome || 'Não definida'; - const tutorEmpresa = empresaData?.tutor_nome || 'N/A'; + setRelatorios(formatados); + } catch (error) { + console.error(error); + Alert.alert('Erro', 'Não foi possível carregar os dados dos estágios.'); + } finally { + if (!isManualRefresh) setLoading(false); + setRefreshing(false); + } + }; + + useFocusEffect(useCallback(() => { fetchRelatorios(); }, [])); + + // 1. GERAR EXCEL (CSV) PERFEITO + const gerarExcelGeral = async () => { + try { + let csvContent = "\uFEFFNumero;Aluno;Turma;Empresa;Horas Feitas;Horas Totais;Nota Empresa;Autoavaliacao\n"; - const nomeAlunoExtracted = alunoData?.nome || aluno_nome || 'N/A'; - const cursoAluno = alunoData?.turma_curso || 'N/A'; - const numeroAluno = alunoData?.n_escola || '--'; - - const horasTotais = infoEstagio?.horas_totais ? `${infoEstagio.horas_totais} Horas` : 'N/A'; - const dataInicioFormatada = infoEstagio?.data_inicio ? new Date(infoEstagio.data_inicio).toLocaleDateString('pt-PT') : 'N/A'; - const dataFimFormatada = infoEstagio?.data_fim ? new Date(infoEstagio.data_fim).toLocaleDateString('pt-PT') : 'N/A'; + 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 logoEPVC_b64 = await getBase64Image(require('../../assets/images/logoepvc2.png')); - const logoEstagios_b64 = await getBase64Image(require('../../assets/images/logo.png')); - const bannerEU_b64 = await getBase64Image(require('../../assets/images/logoepvc.png')); + 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' }); + } + } catch (e) { + Alert.alert('Erro', 'Falha ao gerar o Excel.'); + } + }; + + // 2. GERAR PAUTA GLOBAL EM PDF COM DESIGN OFICIAL + const gerarPautaPDF = async () => { + try { + const logoEPVC_b64 = await getBase64Image(require('../../../assets/images/logoepvc2.png')); + const logoEstagios_b64 = await getBase64Image(require('../../../assets/images/logo.png')); + const bannerEU_b64 = await getBase64Image(require('../../../assets/images/logoepvc.png')); + + let linhasTabela = ''; + relatorios.forEach(r => { + const nota = r.nota_empresa ? `${r.nota_empresa}` : 'Pendente'; + linhasTabela += ` + + ${r.n_escola} + ${r.aluno_nome} + ${r.turma} + ${r.empresa_nome} + ${r.horas_concluidas}/${r.horas_totais}h + ${nota} + + `; + }); + + const htmlContent = ` + + + + + + + + + + + + + + +
+

Pauta Geral de Estágios

+

Formação em Contexto de Trabalho

+
+ +
Resumo de Processos Individuais
+ + + + + + + + + + ${linhasTabela} +
Nome do AlunoTurma/CursoEmpresaHorasNota Final
+ + + + + + `; + + const { uri } = await Print.printToFileAsync({ html: htmlContent }); + const newFileUri = `${FileSystem.documentDirectory}Pauta_Geral_Estagios.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 a Pauta em PDF.'); + } + }; + + // 3. GERAR PDF DOS SUMÁRIOS DIÁRIOS COM DESIGN OFICIAL + 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', 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 diários nem presenças para este aluno.'); + setGerandoPDF(null); + return; + } + + let linhasTabela = ''; + sumarios.forEach((s: any) => { + const dataFormatada = new Date(s.data).toLocaleDateString('pt-PT'); + linhasTabela += ` + + ${dataFormatada} + ${s.tipo} + ${s.sumario || 'Sem descrição submetida.'} + + `; + }); + + const logoEPVC_b64 = await getBase64Image(require('../../../assets/images/logoepvc2.png')); + const logoEstagios_b64 = await getBase64Image(require('../../../assets/images/logo.png')); + const bannerEU_b64 = await getBase64Image(require('../../../assets/images/logoepvc.png')); + + const dataInicioFormatada = r.data_inicio ? new Date(r.data_inicio).toLocaleDateString('pt-PT') : 'N/A'; + const dataFimFormatada = r.data_fim ? new Date(r.data_fim).toLocaleDateString('pt-PT') : 'N/A'; const htmlContent = ` @@ -204,18 +360,9 @@ export default function FichaAvaliacao() { } .eval-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 9px; } - .eval-table th { background-color: #F1F5F9; color: #003049; font-weight: bold; padding: 4px 8px; border: 1px solid #CBD5E1; text-align: left; } + .eval-table th { background-color: #F1F5F9; color: #003049; font-weight: bold; padding: 4px 8px; border: 1px solid #CBD5E1; } .eval-table td { border: 1px solid #CBD5E1; padding: 3px 8px; vertical-align: middle; color: #334155; } - .eval-table th.score, .eval-table td.score { text-align: center; width: 60px; } - .eval-table td.score { font-size: 11px; font-weight: bold; color: #003049; background-color: #F8FAFC; } - .eval-desc { font-size: 8px; color: #64748B; display: block; margin-top: 1px; font-style: italic; } - - .parecer-box { border: 1px solid #CBD5E1; padding: 6px; min-height: 35px; font-size: 9px; color: #334155; background-color: #F8FAFC; border-radius: 3px; margin-bottom: 10px; } - .final-score-container { text-align: right; margin-bottom: 5px; } - .final-score-box { display: inline-block; border: 2px solid #003049; padding: 6px 15px; border-radius: 4px; font-size: 11px; font-weight: bold; color: #003049; background-color: #F8FAFC; } - .final-score-box span { color: #F18721; font-size: 14px; margin-left: 8px; font-weight: 900; } - .footer { text-align: center; padding-top: 5px; border-top: 1px solid #E2E8F0; width: 100%; position: absolute; bottom: 10mm; left: 0; right: 0; margin: 0 auto; width: calc(100% - 30mm); } .banner-img { max-width: 100%; height: auto; max-height: 30px; margin-bottom: 3px; } .footer-text { font-size: 8px; color: #94A3B8; } @@ -227,7 +374,7 @@ export default function FichaAvaliacao() { -

Ficha de Avaliação de Estágio

+

Diário de Bordo

Formação em Contexto de Trabalho

@@ -237,95 +384,38 @@ export default function FichaAvaliacao() { - + - + - + - + - +
Estagiário:${nomeAlunoExtracted} (Nº ${numeroAluno})${r.aluno_nome} (Nº ${r.n_escola}) Turma/Curso:${cursoAluno}${r.turma}
Entidade:${nomeEmpresa}${r.empresa_nome} Tutor(a):${tutorEmpresa}${r.tutor_nome}
Carga Horária:${horasTotais}${r.horas_totais} Horas Período: ${dataInicioFormatada} a ${dataFimFormatada}
-
I. Parâmetros Comportamentais
+
Registo de Atividades Diárias
- - - - - - - - - - - - - - - - - - - - - - + + + + ${linhasTabela}
Critérios de AvaliaçãoClass.
1. Assiduidade e PontualidadeCumprimento de horários e justificação de ausências.${criterios.assiduidade}
2. Relacionamento InterpessoalIntegração na equipa e trato com superiores e colegas.${criterios.relacionamento}
3. Responsabilidade e OrganizaçãoCuidado com o material, posto de trabalho e planeamento.${criterios.responsabilidade}
4. Iniciativa e AutonomiaAção proativa e resolução de problemas sem supervisão.${criterios.iniciativa}
5. Adaptação a Novas TarefasFacilidade e rapidez de aprendizagem perante novos desafios.${criterios.adaptacao}DataNatureza/EstadoSumário / Atividades Desenvolvidas
-
II. Parâmetros Técnicos e Profissionais
- - - - - - - - - - - - - - - - - - - - - - - - - -
Critérios de AvaliaçãoClass.
6. Aplicação de ConhecimentosUtilização prática dos conhecimentos adquiridos no curso.${criterios.conhecimentos}
7. Qualidade e RigorAtenção ao detalhe, brio profissional e ausência de erros.${criterios.qualidade}
8. Interesse e EmpenhoMotivação, dedicação e vontade contínua de evoluir.${criterios.empenho}
9. Uso de Equipamentos e FerramentasDestreza, manuseamento correto e cuidado técnico.${criterios.equipamentos}
10. Segurança e HigieneCumprimento estrito das normas de segurança no trabalho.${criterios.seguranca}
- -
III. Parecer Global da Entidade
-
- ${observacoes ? observacoes.replace(/\n/g, '
') : 'Nenhum parecer qualitativo submetido.'} -
- -
-
- CLASSIFICAÇÃO FINAL ATRIBUÍDA: ${notaFinal} / 20 -
-
- @@ -335,125 +425,161 @@ export default function FichaAvaliacao() { `; 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`; - const fileName = `avaliacao_${estagio_id}_${Date.now()}.pdf`; - const formData = new FormData(); - formData.append('file', { - uri: Platform.OS === 'android' ? uri : uri.replace('file://', ''), - name: fileName, - type: 'application/pdf', - } as any); + await FileSystem.moveAsync({ from: uri, to: newFileUri }); - const { error: uploadError } = await supabase.storage.from('avaliacoes').upload(fileName, formData); - if (uploadError) throw uploadError; - - const { data: urlData } = supabase.storage.from('avaliacoes').getPublicUrl(fileName); - const { error: dbError } = await supabase - .from('estagios') - .update({ nota_final: notaNum, avaliacao_url: urlData.publicUrl }) - .eq('id', estagio_id); - - if (dbError) throw dbError; - - Alert.alert('Sucesso', 'Ficha oficial gerada com sucesso e anexada ao processo do aluno.'); - if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(uri); - router.back(); + if (await Sharing.isAvailableAsync()) { + await Sharing.shareAsync(newFileUri, { + mimeType: 'application/pdf', + dialogTitle: `Partilhar Diário de Bordo de ${r.aluno_nome}`, + UTI: 'com.adobe.pdf' + }); + } } catch (e) { - console.error(e); - Alert.alert('Erro', 'Ocorreu uma falha crítica na geração do PDF.'); + console.error("Erro a gerar Sumários PDF:", e); + Alert.alert('Erro', 'Não foi possível ler os registos.'); } finally { - setLoading(false); + setGerandoPDF(null); } }; return ( - + - + + + router.back()}> + + + Gestão de Avaliações + + + + { setRefreshing(true); fetchRelatorios(true); }} tintColor={cores.azulMarinho} />} + > - - router.back()} style={{ padding: 5 }}> - Ficha de Avaliação - - + {/* BOTÕES DE EXPORTAÇÃO GLOBAL */} + + + + Excel (CSV) + - - - - - - {aluno_nome} - Avalie os 10 parâmetros oficiais. - - - - Critérios Comportamentais - - - - - - - - - Critérios Técnicos - - - - - - - - - Classificação Final (0-20) - - - Nota Quantitativa - - - - - Parecer Qualitativo - - - - - - {loading ? : Finalizar e Gerar Documento} + + + Pauta em PDF - + Processos Individuais + + {loading && !refreshing ? ( + + ) : relatorios.length === 0 ? ( + Nenhum estágio encontrado. + ) : ( + relatorios.map((r, index) => ( + + + + + {r.aluno_nome.charAt(0)} + + + {r.aluno_nome} + {r.empresa_nome} • {r.turma} + + + + + + {/* 1. AVALIAÇÃO DA EMPRESA */} + + + 1. Avaliação da Empresa + {r.nota_empresa ? ( + {r.nota_empresa} Val. + ) : ( + Pendente + )} + + {r.pdf_empresa && ( + WebBrowser.openBrowserAsync(r.pdf_empresa)}> + + Ver Ficha de Avaliação + + )} + + + {/* 2. AUTOAVALIAÇÃO DO ALUNO */} + + + 2. Autoavaliação do Aluno + Em Breve + + + + {/* 3. DIÁRIO DE BORDO (PDF) */} + + + 3. Diário de Bordo + {r.horas_concluidas}h Registadas + + gerarSumariosPDF(r)} /* PASSEI A VARIÁVEL COMPLETA AQUI! */ + disabled={gerandoPDF === r.id_estagio} + > + {gerandoPDF === r.id_estagio ? ( + + ) : ( + <> + + Exportar Histórico (PDF) + + )} + + + + + )) + )} + + ); } const styles = StyleSheet.create({ - header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 }, - headerTitle: { fontSize: 20, fontWeight: '900' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, borderWidth: 1, marginBottom: 25, gap: 12 }, - alunoCardText: { fontSize: 17, fontWeight: '900' }, - sectionAppTitle: { fontSize: 13, fontWeight: '900', color: '#64748B', marginBottom: 12, marginLeft: 5, textTransform: 'uppercase' }, - card: { padding: 20, paddingBottom: 5, borderRadius: 24, borderWidth: 1, marginBottom: 30 }, - criterioContainer: { marginBottom: 20 }, - criterioLabel: { fontSize: 14, fontWeight: '700', marginBottom: 12 }, - botoesContainer: { flexDirection: 'row', justifyContent: 'space-between' }, - botaoNota: { width: 48, height: 48, borderRadius: 14, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center' }, - botaoTexto: { fontSize: 18, fontWeight: '800' }, - notaCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 30 }, - notaLabel: { fontSize: 16, fontWeight: '900' }, - gradeInput: { fontSize: 24, fontWeight: '900', borderWidth: 1.5, borderRadius: 16, width: 80, textAlign: 'center', paddingVertical: 12 }, - textArea: { borderRadius: 24, borderWidth: 1, padding: 20, minHeight: 120, fontSize: 15 }, - footerBtnContainer: { padding: 20, borderTopWidth: 1, borderColor: 'rgba(0,0,0,0.05)' }, - btnSubmit: { paddingVertical: 18, borderRadius: 20, alignItems: 'center', justifyContent: 'center', elevation: 3 }, - btnSubmitText: { color: '#FFF', fontSize: 17, fontWeight: '800' } + safeArea: { flex: 1 }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 15, paddingBottom: 10 }, + btnVoltar: { padding: 5, marginLeft: -5 }, + headerTitle: { fontSize: 18, fontWeight: '900' }, + scrollContent: { padding: 20, paddingBottom: 40 }, + + 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 }, + + sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 15, marginLeft: 5, letterSpacing: 1 }, + + card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 20 }, + cardHeader: { flexDirection: 'row', alignItems: 'center' }, + avatar: { width: 44, height: 44, borderRadius: 22, justifyContent: 'center', alignItems: 'center' }, + alunoName: { fontSize: 16, fontWeight: '900' }, + 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', alignSelf: 'flex-start', paddingVertical: 6, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, marginTop: 10, gap: 6 }, + textoAcao: { fontSize: 12, fontWeight: '800' } }); \ No newline at end of file diff --git a/app/Professor/Alunos/relatorios.tsx b/app/Professor/Alunos/relatorios.tsx new file mode 100644 index 0000000..563b852 --- /dev/null +++ b/app/Professor/Alunos/relatorios.tsx @@ -0,0 +1,419 @@ +// app/Professor/Alunos/relatorios.tsx +import { Ionicons } from '@expo/vector-icons'; +import { useFocusEffect } from '@react-navigation/native'; +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, + Alert, + RefreshControl, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; +import { supabase } from '../../../lib/supabase'; +import { useTheme } from '../../../themecontext'; + +export default function GestaoRelatorios() { + const { isDarkMode } = useTheme(); + const router = useRouter(); + + const [relatorios, setRelatorios] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [gerandoPDF, setGerandoPDF] = useState(null); + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#0D2235', + textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + azulMarinho: '#003049', + verdeAgua: '#71BEB3', + laranja: '#F18721', + }), [isDarkMode]); + + 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) + `) + .order('data_inicio', { ascending: false }); + + if (error) throw error; + + const formatados = data?.map((estagio: any) => { + const aluno = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos; + const empresa = Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas; + + return { + id_estagio: estagio.id, + aluno_id: aluno?.id, + aluno_nome: aluno?.nome || 'Desconhecido', + turma: aluno?.turma_curso || 'N/A', + n_escola: aluno?.n_escola || '--', + empresa_nome: empresa?.nome || 'N/A', + horas_totais: estagio.horas_totais || 0, + horas_concluidas: estagio.horas_concluidas || 0, + nota_empresa: estagio.nota_final, + pdf_empresa: estagio.avaliacao_url + }; + }) || []; + + setRelatorios(formatados); + } catch (error) { + console.error(error); + Alert.alert('Erro', 'Não foi possível carregar os dados dos estágios.'); + } finally { + if (!isManualRefresh) setLoading(false); + setRefreshing(false); + } + }; + + 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 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' }); + } + } catch (e) { + Alert.alert('Erro', 'Falha ao gerar o Excel.'); + } + }; + + // 2. GERAR PAUTA GLOBAL EM PDF + const gerarPautaPDF = async () => { + try { + let linhasTabela = ''; + relatorios.forEach(r => { + const nota = r.nota_empresa ? `${r.nota_empresa}` : 'Pendente'; + linhasTabela += ` + + ${r.n_escola} + ${r.aluno_nome} + ${r.turma} + ${r.empresa_nome} + ${r.horas_concluidas}/${r.horas_totais}h + ${nota} + + `; + }); + + const htmlContent = ` + + + + + + + +

Pauta Geral de Avaliações - Estágios

+
Data de Emissão: ${new Date().toLocaleDateString('pt-PT')}
+ + + + + + + + + + ${linhasTabela} +
Nome do AlunoTurma/CursoEmpresaHorasNota Final
+ + + `; + + const { uri } = await Print.printToFileAsync({ html: htmlContent }); + const newFileUri = `${FileSystem.documentDirectory}Pauta_Geral_Estagios.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 a Pauta em PDF.'); + } + }; + + // 3. GERAR PDF DOS SUMÁRIOS DIÁRIOS + const gerarSumariosPDF = async (estagio_id: string, aluno_id: string, aluno_nome: string) => { + setGerandoPDF(estagio_id); + try { + let { data: sumarios, error } = await supabase + .from('registos_diarios') + .select('data, tipo, sumario') + .eq('estagio_id', estagio_id) + .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 (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 diários nem presenças para este aluno.'); + setGerandoPDF(null); + return; + } + + let linhasTabela = ''; + sumarios.forEach((s: any) => { + const dataFormatada = new Date(s.data).toLocaleDateString('pt-PT'); + linhasTabela += ` + + ${dataFormatada} + ${s.tipo} + ${s.sumario || 'Sem descrição submetida.'} + + `; + }); + + const htmlContent = ` + + + + + + + +

Diário de Bordo

+

Estagiário: ${aluno_nome}

+ + + + + + + ${linhasTabela} +
DataNatureza/EstadoAtividades Desenvolvidas
+ + + `; + + const { uri } = await Print.printToFileAsync({ html: htmlContent }); + const safeName = 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' + }); + } + + } catch (e) { + console.error("Erro a gerar Sumários PDF:", e); + Alert.alert('Erro', 'Não foi possível ler os registos.'); + } finally { + setGerandoPDF(null); + } + }; + + return ( + + + + + router.back()}> + + + Gestão de Avaliações + + + + { setRefreshing(true); fetchRelatorios(true); }} tintColor={cores.azulMarinho} />} + > + + {/* BOTÕES DE EXPORTAÇÃO GLOBAL */} + + + + Excel (CSV) + + + + + Pauta em PDF + + + + Processos Individuais + + {loading && !refreshing ? ( + + ) : relatorios.length === 0 ? ( + Nenhum estágio encontrado. + ) : ( + relatorios.map((r, index) => ( + + + + + {r.aluno_nome.charAt(0)} + + + {r.aluno_nome} + {r.empresa_nome} • {r.turma} + + + + + + {/* 1. AVALIAÇÃO DA EMPRESA */} + + + 1. Avaliação da Empresa + {r.nota_empresa ? ( + {r.nota_empresa} Val. + ) : ( + Pendente + )} + + {r.pdf_empresa && ( + WebBrowser.openBrowserAsync(r.pdf_empresa)}> + + Ver Ficha de Avaliação + + )} + + + {/* 2. AUTOAVALIAÇÃO DO ALUNO */} + + + 2. Autoavaliação do Aluno + Em Breve + + + + {/* 3. DIÁRIO DE BORDO (PDF) - AGORA COM O ALUNO_ID! */} + + + 3. Diário de Bordo + {r.horas_concluidas}h Registadas + + gerarSumariosPDF(r.id_estagio, r.aluno_id, r.aluno_nome)} + disabled={gerandoPDF === r.id_estagio} + > + {gerandoPDF === r.id_estagio ? ( + + ) : ( + <> + + Exportar Histórico (PDF) + + )} + + + + + )) + )} + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { flex: 1 }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 15, paddingBottom: 10 }, + btnVoltar: { padding: 5, marginLeft: -5 }, + headerTitle: { fontSize: 18, fontWeight: '900' }, + scrollContent: { padding: 20, paddingBottom: 40 }, + + 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 }, + + sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 15, marginLeft: 5, letterSpacing: 1 }, + + card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 20 }, + cardHeader: { flexDirection: 'row', alignItems: 'center' }, + avatar: { width: 44, height: 44, borderRadius: 22, justifyContent: 'center', alignItems: 'center' }, + alunoName: { fontSize: 16, fontWeight: '900' }, + 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', alignSelf: 'flex-start', paddingVertical: 6, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, marginTop: 10, gap: 6 }, + textoAcao: { fontSize: 12, fontWeight: '800' } +}); \ No newline at end of file diff --git a/app/Professor/ProfessorHome.tsx b/app/Professor/ProfessorHome.tsx index 140923d..a2617f6 100644 --- a/app/Professor/ProfessorHome.tsx +++ b/app/Professor/ProfessorHome.tsx @@ -127,7 +127,7 @@ export default function ProfessorHome() { router.push('/Professor/Alunos/Presencas')} cores={cores} corDestaque={cores.azul} /> router.push('/Professor/Alunos/Sumarios')} cores={cores} corDestaque={cores.laranja} /> router.push('/Professor/Alunos/Faltas')} cores={cores} corDestaque="#EF4444" /> - router.push('')} cores={cores} corDestaque={cores.azul} /> + router.push('/Professor/Alunos/relatorios')} cores={cores} corDestaque={cores.azul} />