atualizacoes
This commit is contained in:
@@ -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' }
|
||||
});
|
||||
419
app/Professor/Alunos/relatorios.tsx
Normal file
419
app/Professor/Alunos/relatorios.tsx
Normal 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' }
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user