atualizacoes

This commit is contained in:
2026-05-08 10:40:28 +01:00
parent d6a73c3571
commit f8d12176fa
3 changed files with 836 additions and 291 deletions

View File

@@ -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<any[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [gerandoPDF, setGerandoPDF] = useState<string | null>(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 }) => (
<View style={styles.criterioContainer}>
<Text style={[styles.criterioLabel, { color: cores.texto }]}>{label}</Text>
<View style={styles.botoesContainer}>
{[1, 2, 3, 4, 5].map((num) => {
const selecionado = criterios[field] === num;
return (
<TouchableOpacity
key={num}
activeOpacity={0.7}
onPress={() => setCriterios({ ...criterios, [field]: num })}
style={[
styles.botaoNota,
{
borderColor: selecionado ? cores.azulMarinho : cores.borda,
backgroundColor: selecionado ? cores.azulMarinho : cores.card,
}
]}
>
<Text style={[
styles.botaoTexto,
{ color: selecionado ? '#FFF' : cores.textoSecundario }
]}>
{num}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
);
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 ? `<strong>${r.nota_empresa}</strong>` : '<span style="color:#F18721">Pendente</span>';
linhasTabela += `
<tr>
<td style="text-align: center;">${r.n_escola}</td>
<td><strong>${r.aluno_nome}</strong></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;" class="score">${nota}</td>
</tr>
`;
});
const htmlContent = `
<!DOCTYPE html>
<html lang="pt-PT">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 0; }
html, body { height: 99%; overflow: hidden; }
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 12mm 15mm;
color: #1E293B;
line-height: 1.2;
background-color: #fff;
font-size: 10px;
box-sizing: border-box;
}
.header-table { width: 100%; border-bottom: 2px solid #003049; padding-bottom: 5px; margin-bottom: 8px; }
.header-table td { vertical-align: middle; }
.logo-epvc { width: 110px; display: block; }
.logo-estagios { width: 160px; display: block; float: right; margin-top: -5px; margin-right: -5px; }
.header-center { text-align: center; }
.header-center h1 { color: #003049; margin: 0; font-size: 15px; font-weight: 900; text-transform: uppercase; }
.header-center h2 { color: #F18721; margin: 2px 0 0; font-size: 9px; font-weight: 800; text-transform: uppercase; }
.section-title {
background-color: #003049;
color: white;
padding: 4px 8px;
font-size: 9px;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 5px;
margin-top: 15px;
border-radius: 3px;
}
.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: 6px 8px; border: 1px solid #CBD5E1; text-align: left; text-transform: uppercase; }
.eval-table td { border: 1px solid #CBD5E1; padding: 6px 8px; vertical-align: middle; color: #334155; }
.eval-table td.score { font-size: 11px; color: #003049; background-color: #F8FAFC; }
.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; }
</style>
</head>
<body>
<table class="header-table">
<tr>
<td style="width: 25%;"><img src="${logoEPVC_b64}" class="logo-epvc" /></td>
<td style="width: 50%;" class="header-center">
<h1>Pauta Geral de Estágios</h1>
<h2>Formação em Contexto de Trabalho</h2>
</td>
<td style="width: 25%; text-align: right;"><img src="${logoEstagios_b64}" class="logo-estagios" /></td>
</tr>
</table>
<div class="section-title">Resumo de Processos Individuais</div>
<table class="eval-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>
<div class="footer">
<img src="${bannerEU_b64}" class="banner-img" />
<div class="footer-text">
Documento gerado digitalmente via plataforma Estágios+ EPVC.<br/>
Data de emissão: ${new Date().toLocaleDateString('pt-PT')}
</div>
</div>
</body>
</html>
`;
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 += `
<tr>
<td style="text-align: center;">${dataFormatada}</td>
<td style="text-align: center;"><strong>${s.tipo}</strong></td>
<td>${s.sumario || '<em>Sem descrição submetida.</em>'}</td>
</tr>
`;
});
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 = `
<!DOCTYPE html>
@@ -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() {
<tr>
<td style="width: 25%;"><img src="${logoEPVC_b64}" class="logo-epvc" /></td>
<td style="width: 50%;" class="header-center">
<h1>Ficha de Avaliação de Estágio</h1>
<h1>Diário de Bordo</h1>
<h2>Formação em Contexto de Trabalho</h2>
</td>
<td style="width: 25%; text-align: right;"><img src="${logoEstagios_b64}" class="logo-estagios" /></td>
@@ -237,95 +384,38 @@ export default function FichaAvaliacao() {
<table class="info-table">
<tr>
<td class="label">Estagiário:</td>
<td class="value"><strong>${nomeAlunoExtracted}</strong> (Nº ${numeroAluno})</td>
<td class="value"><strong>${r.aluno_nome}</strong> (Nº ${r.n_escola})</td>
<td class="label">Turma/Curso:</td>
<td class="value">${cursoAluno}</td>
<td class="value">${r.turma}</td>
</tr>
<tr>
<td class="label">Entidade:</td>
<td class="value"><strong>${nomeEmpresa}</strong></td>
<td class="value"><strong>${r.empresa_nome}</strong></td>
<td class="label">Tutor(a):</td>
<td class="value">${tutorEmpresa}</td>
<td class="value">${r.tutor_nome}</td>
</tr>
<tr>
<td class="label">Carga Horária:</td>
<td class="value"><strong>${horasTotais}</strong></td>
<td class="value"><strong>${r.horas_totais} Horas</strong></td>
<td class="label">Período:</td>
<td class="value">${dataInicioFormatada} a ${dataFimFormatada}</td>
</tr>
</table>
<div class="section-title">I. Parâmetros Comportamentais</div>
<div class="section-title">Registo de Atividades Diárias</div>
<table class="eval-table">
<tr>
<th>Critérios de Avaliação</th>
<th class="score">Class.</th>
</tr>
<tr>
<td><strong>1. Assiduidade e Pontualidade</strong><span class="eval-desc">Cumprimento de horários e justificação de ausências.</span></td>
<td class="score">${criterios.assiduidade}</td>
</tr>
<tr>
<td><strong>2. Relacionamento Interpessoal</strong><span class="eval-desc">Integração na equipa e trato com superiores e colegas.</span></td>
<td class="score">${criterios.relacionamento}</td>
</tr>
<tr>
<td><strong>3. Responsabilidade e Organização</strong><span class="eval-desc">Cuidado com o material, posto de trabalho e planeamento.</span></td>
<td class="score">${criterios.responsabilidade}</td>
</tr>
<tr>
<td><strong>4. Iniciativa e Autonomia</strong><span class="eval-desc">Ação proativa e resolução de problemas sem supervisão.</span></td>
<td class="score">${criterios.iniciativa}</td>
</tr>
<tr>
<td><strong>5. Adaptação a Novas Tarefas</strong><span class="eval-desc">Facilidade e rapidez de aprendizagem perante novos desafios.</span></td>
<td class="score">${criterios.adaptacao}</td>
<th style="width: 15%; text-align: center;">Data</th>
<th style="width: 20%; text-align: center;">Natureza/Estado</th>
<th style="width: 65%; text-align: left;">Sumário / Atividades Desenvolvidas</th>
</tr>
${linhasTabela}
</table>
<div class="section-title">II. Parâmetros Técnicos e Profissionais</div>
<table class="eval-table">
<tr>
<th>Critérios de Avaliação</th>
<th class="score">Class.</th>
</tr>
<tr>
<td><strong>6. Aplicação de Conhecimentos</strong><span class="eval-desc">Utilização prática dos conhecimentos adquiridos no curso.</span></td>
<td class="score">${criterios.conhecimentos}</td>
</tr>
<tr>
<td><strong>7. Qualidade e Rigor</strong><span class="eval-desc">Atenção ao detalhe, brio profissional e ausência de erros.</span></td>
<td class="score">${criterios.qualidade}</td>
</tr>
<tr>
<td><strong>8. Interesse e Empenho</strong><span class="eval-desc">Motivação, dedicação e vontade contínua de evoluir.</span></td>
<td class="score">${criterios.empenho}</td>
</tr>
<tr>
<td><strong>9. Uso de Equipamentos e Ferramentas</strong><span class="eval-desc">Destreza, manuseamento correto e cuidado técnico.</span></td>
<td class="score">${criterios.equipamentos}</td>
</tr>
<tr>
<td><strong>10. Segurança e Higiene</strong><span class="eval-desc">Cumprimento estrito das normas de segurança no trabalho.</span></td>
<td class="score">${criterios.seguranca}</td>
</tr>
</table>
<div class="section-title">III. Parecer Global da Entidade</div>
<div class="parecer-box">
${observacoes ? observacoes.replace(/\n/g, '<br/>') : '<em>Nenhum parecer qualitativo submetido.</em>'}
</div>
<div class="final-score-container">
<div class="final-score-box">
CLASSIFICAÇÃO FINAL ATRIBUÍDA: <span>${notaFinal} / 20</span>
</div>
</div>
<div class="footer">
<img src="${bannerEU_b64}" class="banner-img" />
<div class="footer-text">
Documento gerado digitalmente pela Entidade de Acolhimento via plataforma Estágios+ EPVC.<br/>
Documento gerado digitalmente via plataforma Estágios+ EPVC.<br/>
Data de emissão: ${new Date().toLocaleDateString('pt-PT')}
</div>
</div>
@@ -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 (
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }} edges={['top', 'left', 'right']}>
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
<View style={styles.header}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => 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>
<View style={{ width: 24 }} />
</View>
<ScrollView
contentContainerStyle={styles.scrollContent}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchRelatorios(true); }} tintColor={cores.azulMarinho} />}
>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={{ padding: 5 }}><Ionicons name="arrow-back" size={24} color={cores.texto} /></TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha de Avaliação</Text>
<View style={{ width: 24 }} />
</View>
{/* 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>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<View style={[styles.alunoCard, { backgroundColor: cores.verdeAgua + '15', borderColor: cores.verdeAgua + '40' }]}>
<Ionicons name="person-circle-outline" size={32} color={cores.azulMarinho} />
<View>
<Text style={[styles.alunoCardText, { color: cores.azulMarinho }]}>{aluno_nome}</Text>
<Text style={{ fontSize: 12, color: cores.azulMarinho, opacity: 0.7 }}>Avalie os 10 parâmetros oficiais.</Text>
</View>
</View>
<Text style={styles.sectionAppTitle}>Critérios Comportamentais</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<ClassificacaoRow label="Assiduidade e Pontualidade" field="assiduidade" />
<ClassificacaoRow label="Relacionamento Interpessoal" field="relacionamento" />
<ClassificacaoRow label="Responsabilidade e Organização" field="responsabilidade" />
<ClassificacaoRow label="Iniciativa e Autonomia" field="iniciativa" />
<ClassificacaoRow label="Adaptação a Novas Tarefas" field="adaptacao" />
</View>
<Text style={styles.sectionAppTitle}>Critérios Técnicos</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<ClassificacaoRow label="Aplicação de Conhecimentos" field="conhecimentos" />
<ClassificacaoRow label="Qualidade e Rigor" field="qualidade" />
<ClassificacaoRow label="Interesse e Empenho" field="empenho" />
<ClassificacaoRow label="Uso de Equipamentos/Ferramentas" field="equipamentos" />
<ClassificacaoRow label="Segurança e Higiene" field="seguranca" />
</View>
<Text style={styles.sectionAppTitle}>Classificação Final (0-20)</Text>
<View style={[styles.notaCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={{ flex: 1 }}>
<Text style={[styles.notaLabel, { color: cores.texto }]}>Nota Quantitativa</Text>
</View>
<TextInput
style={[styles.gradeInput, { color: cores.laranja, borderColor: cores.laranja + '50', backgroundColor: cores.laranja + '05' }]}
keyboardType="numeric" maxLength={2} placeholder="--" placeholderTextColor={cores.textoSecundario} value={notaFinal} onChangeText={setNotaFinal}
/>
</View>
<Text style={styles.sectionAppTitle}>Parecer Qualitativo</Text>
<TextInput
style={[styles.textArea, { backgroundColor: cores.card, borderColor: cores.borda, color: cores.texto }]}
multiline placeholder="Deixe um comentário institucional sobre a prestação do aluno..." placeholderTextColor={cores.textoSecundario}
value={observacoes} onChangeText={setObservacoes} textAlignVertical="top"
/>
</ScrollView>
<View style={[styles.footerBtnContainer, { backgroundColor: cores.fundo }]}>
<TouchableOpacity style={[styles.btnSubmit, { backgroundColor: cores.azulMarinho }]} onPress={submeterAvaliacao} disabled={loading}>
{loading ? <ActivityIndicator color="#FFF" /> : <Text style={styles.btnSubmitText}>Finalizar e Gerar Documento</Text>}
<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>
</KeyboardAvoidingView>
<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>
</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>
</TouchableOpacity>
)}
</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) */}
<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)} /* PASSEI A VARIÁVEL COMPLETA AQUI! */
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>
</View>
))
)}
</ScrollView>
</SafeAreaView>
);
}
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' }
});

View File

@@ -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<any[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [gerandoPDF, setGerandoPDF] = useState<string | null>(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 ? `<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 htmlContent = `
<!DOCTYPE html>
<html>
<head>
<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; }
</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>
</body>
</html>
`;
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 += `
<tr>
<td>${dataFormatada}</td>
<td><strong>${s.tipo}</strong></td>
<td>${s.sumario || 'Sem descrição submetida.'}</td>
</tr>
`;
});
const htmlContent = `
<!DOCTYPE html>
<html>
<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; }
</style>
</head>
<body>
<h1>Diário de Bordo</h1>
<h2>Estagiário: ${aluno_nome}</h2>
<table>
<tr>
<th style="width: 15%;">Data</th>
<th style="width: 20%;">Natureza/Estado</th>
<th style="width: 65%;">Atividades Desenvolvidas</th>
</tr>
${linhasTabela}
</table>
</body>
</html>
`;
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 (
<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()}>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Gestão de Avaliações</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView
contentContainerStyle={styles.scrollContent}
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>
</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>
</TouchableOpacity>
)}
</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. 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.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>
</View>
))
)}
</ScrollView>
</SafeAreaView>
);
}
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' }
});

View File

@@ -127,7 +127,7 @@ export default function ProfessorHome() {
<MenuCard icon="calendar" title="Presenças" subtitle="Verifica a presença e a localização dos alunos" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} corDestaque={cores.azul} />
<MenuCard icon="document-text" title="Sumários" subtitle="Verifica os sumários dos alunos" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} corDestaque={cores.laranja} />
<MenuCard icon="alert-circle" title="Faltas" subtitle="Verifica as faltas e as justificações" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} corDestaque="#EF4444" />
<MenuCard icon="alert-circle" title="Relatórios" subtitle="Verifica e obtém relatórios" onPress={() => router.push('')} cores={cores} corDestaque={cores.azul} />
<MenuCard icon="alert-circle" title="Relatórios" subtitle="Verifica e obtém relatórios" onPress={() => router.push('/Professor/Alunos/relatorios')} cores={cores} corDestaque={cores.azul} />
</View>
</View>