update - apresentação

This commit is contained in:
2026-06-15 16:58:26 +01:00
parent 4d6a460ee4
commit 967b238955
2 changed files with 245 additions and 414 deletions

View File

@@ -1,5 +1,4 @@
// app/Empresas/fichaAvaliacao.tsx
// Navegar para esta página passando: { estagio_id, aluno_nome, aluno_turma, n_escola }
import { Ionicons } from '@expo/vector-icons';
import { Asset } from 'expo-asset';
@@ -25,44 +24,31 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { supabase } from '../../lib/supabase';
import { useTheme } from '../../themecontext';
// ─── Perguntas de Avaliação ────────────────────────────────────────────────────
const PERGUNTAS = [
{ id: 'p1', categoria: 'Competências Técnicas', texto: 'Domínio das tarefas e conhecimentos técnicos exigidos.' },
{ id: 'p2', categoria: 'Competências Técnicas', texto: 'Capacidade de aprendizagem e adaptação a novas situações.' },
{ id: 'p3', categoria: 'Competências Técnicas', texto: 'Qualidade e rigor do trabalho realizado.' },
{ id: 'p4', categoria: 'Atitude Profissional', texto: 'Pontualidade, assiduidade e cumprimento de horários.' },
{ id: 'p5', categoria: 'Atitude Profissional', texto: 'Iniciativa, proatividade e autonomia nas tarefas.' },
{ id: 'p6', categoria: 'Atitude Profissional', texto: 'Responsabilidade e cumprimento das normas da empresa.' },
{ id: 'p7', categoria: 'Relacionamento', texto: 'Relacionamento com colegas e integração na equipa.' },
{ id: 'p8', categoria: 'Relacionamento', texto: 'Comunicação e postura com clientes e superiores.' },
{ id: 'p9', categoria: 'Desenvolvimento Pessoal', texto: 'Capacidade de gerir dificuldades e resolver problemas.' },
{ id: 'p10', categoria: 'Desenvolvimento Pessoal', texto: 'Evolução e progresso ao longo do período de estágio.' },
{ id: 'p1', texto: 'Compreensão das tarefas propostas.' },
{ id: 'p2', texto: 'Grau de colaboração na execução das tarefas propostas.' },
{ id: 'p3', texto: 'Planeamento e organização de atividades.' },
{ id: 'p4', texto: 'Selecção e/ou adaptação dos materiais de acordo com as atividades.' },
{ id: 'p5', texto: 'Cuidado com os materiais e equipamentos.' },
{ id: 'p6', texto: 'Aplicação de normas de higiene e segurança no local de trabalho.' },
{ id: 'p7', texto: 'Rentabilização do tempo.' },
{ id: 'p8', texto: 'Responsabilidade e autonomia no desenvolvimento e execução das atividades.' },
{ id: 'p9', texto: 'Assiduidade e pontualidade.' },
{ id: 'p10', texto: 'Apresentação e higiene pessoal.' },
{ id: 'p11', texto: 'Comunicação e relações interpessoais.' },
{ id: 'p12', texto: 'Capacidade de integração no grupo de trabalho.' },
{ id: 'p13', texto: 'Competências inerentes ao posto de trabalho.' },
{ id: 'p14', texto: 'Desenvolvimento de novas competências.' },
];
const ESCALA = [
{ valor: 1, label: 'Insatisfatório' },
{ valor: 2, label: 'A Melhorar' },
{ valor: 3, label: 'Satisfatório' },
{ valor: 4, label: 'Bom' },
const COLUNAS_ESCALA = [
{ valor: 5, label: 'Excelente' },
{ valor: 4, label: 'Muito Bom' },
{ valor: 3, label: 'Bom' },
{ valor: 2, label: 'Médio' },
{ valor: 1, label: 'Insuficiente' },
];
// ─── Helper: imagem em base64 (para incluir no PDF) ────────────────────────────
const getBase64Image = async (imageModule: any) => {
try {
const asset = Asset.fromModule(imageModule);
await asset.downloadAsync();
const fileUri = asset.localUri || asset.uri;
if (!fileUri) return '';
const base64 = await FileSystem.readAsStringAsync(fileUri, { encoding: 'base64' });
return `data:image/png;base64,${base64.replace(/(\r\n|\n|\r)/gm, '')}`;
} catch (e) {
console.error('Erro no Base64:', e);
return '';
}
};
// ─── Componente Principal ──────────────────────────────────────────────────────
export default function FichaAvaliacao() {
const { isDarkMode } = useTheme();
const router = useRouter();
@@ -73,7 +59,6 @@ export default function FichaAvaliacao() {
n_escola: string;
}>();
// Estados locais dinâmicos para contornar o envio incorreto de UUIDs por parâmetro
const [alunoNome, setAlunoNome] = useState(params.aluno_nome || 'Aluno');
const [alunoTurma, setAlunoTurma] = useState(params.aluno_turma || '—');
const [numEscola, setNumEscola] = useState(params.n_escola || '—');
@@ -85,6 +70,10 @@ export default function FichaAvaliacao() {
const [gerandoPDF, setGerandoPDF] = useState(false);
const [loadingDados, setLoadingDados] = useState(true);
const [imgEscolaB64, setImgEscolaB64] = useState('');
const [imgAppB64, setImgAppB64] = useState('');
const [imgFundoB64, setImgFundoB64] = useState('');
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
card: isDarkMode ? '#161618' : '#FFFFFF',
@@ -92,20 +81,39 @@ export default function FichaAvaliacao() {
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azulMarinho: '#003049',
verdeAgua: '#71BEB3',
laranja: '#F18721',
vermelho: '#EF4444',
verde: '#22C55E',
inputFundo: isDarkMode ? '#1E1E20' : '#F8FAFC',
}), [isDarkMode]);
// ─── Carregar dados existentes ao entrar na página com Correção de Fallback ───
const preCarregarImagem = async (modulo: any) => {
try {
const asset = Asset.fromModule(modulo);
await asset.downloadAsync();
const uri = asset.localUri || asset.uri;
if (!uri) return '';
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: 'base64' });
return `data:image/png;base64,${b64.replace(/[\r\n\x0B\x0C\x85]/g, '')}`;
} catch (e) {
console.warn(e);
return '';
}
};
useEffect(() => {
async function carregarImagens() {
setImgEscolaB64(await preCarregarImagem(require('../../assets/images/logoepvc2.png')));
setImgAppB64(await preCarregarImagem(require('../../assets/images/logo.png')));
setImgFundoB64(await preCarregarImagem(require('../../assets/images/logoepvc.png')));
}
carregarImagens();
}, []);
useEffect(() => {
async function carregarDadosCompletos() {
if (!params.estagio_id) return;
try {
// 1. Procurar se já existe avaliação gravada
const { data: avaliacao, error: errAvaliacao } = await supabase
.from('avaliacoes_empresa')
.select('respostas, nota_final, observacoes, aluno_nome, aluno_turma, aluno_n_escola')
@@ -116,30 +124,19 @@ export default function FichaAvaliacao() {
if (avaliacao) {
if (avaliacao.respostas) setRespostas(avaliacao.respostas as Record<string, number>);
if (avaliacao.nota_final !== undefined && avaliacao.nota_final !== null) {
const notaNum = Number(avaliacao.nota_final);
setNotaFinal(isNaN(notaNum) ? '' : notaNum.toString());
setNotaFinal(Number(avaliacao.nota_final).toString());
}
if (avaliacao.observacoes) setObservacoes(avaliacao.observacoes);
if (avaliacao.aluno_nome) setAlunoNome(avaliacao.aluno_nome);
if (avaliacao.aluno_turma) setAlunoTurma(avaliacao.aluno_turma);
if (avaliacao.aluno_n_escola) setNumEscola(avaliacao.aluno_n_escola.toString());
}
// 2. Fallback robusto olhando o ERD do Supabase: busca real do 'n_escola' e 'turma_curso'
// Executado se o parâmetro recebido estiver em falta ou for detetado um UUID longo (>15 caracteres)
if (!params.aluno_turma || !params.n_escola || params.n_escola.length > 15) {
const { data: estagio, error: errEstagio } = await supabase
.from('estagios')
.select(`
alunos (
nome,
turma_curso,
n_escola
)
`)
.select(`alunos ( nome, turma_curso, n_escola )`)
.eq('id', params.estagio_id)
.maybeSingle();
@@ -153,26 +150,13 @@ export default function FichaAvaliacao() {
}
}
} catch (e) {
console.error('Erro ao carregar dados da avaliação:', e);
console.error(e);
} finally {
setLoadingDados(false);
}
}
carregarDadosCompletos();
}, [params.estagio_id, params.n_escola, params.aluno_turma]);
const corEscala = (valor: number, selecionado: boolean) => {
if (!selecionado) return { bg: cores.inputFundo, borda: cores.borda, texto: cores.textoSecundario };
const mapa: Record<number, { bg: string; borda: string; texto: string }> = {
1: { bg: '#FEE2E2', borda: '#EF4444', texto: '#B91C1C' },
2: { bg: '#FEF3C7', borda: '#F59E0B', texto: '#B45309' },
3: { bg: '#FEF9C3', borda: '#EAB308', texto: '#854D0E' },
4: { bg: '#DCFCE7', borda: '#22C55E', texto: '#15803D' },
5: { bg: '#D1FAE5', borda: '#10B981', texto: '#065F46' },
};
return mapa[valor];
};
}, [params.estagio_id]);
const notaValida = useMemo(() => {
if (!notaFinal.trim()) return false;
@@ -183,25 +167,14 @@ export default function FichaAvaliacao() {
const todasRespondidas = PERGUNTAS.every(p => respostas[p.id] !== undefined);
const podeSometer = todasRespondidas && notaValida;
const categorias = useMemo(() => {
const mapa: Record<string, typeof PERGUNTAS> = {};
PERGUNTAS.forEach(p => {
if (!mapa[p.categoria]) mapa[p.categoria] = [];
mapa[p.categoria].push(p);
});
return mapa;
}, []);
// ─── Submeter Avaliação ──────────────────────────────────────────────────────
const handleSubmit = async () => {
const handleSubmit = () => {
if (!podeSometer) {
Alert.alert('Atenção', 'Preenche todas as questões e insere uma nota final válida (020).');
Alert.alert('Atenção', 'Responde às 14 perguntas e insere a nota final (0-20).');
return;
}
Alert.alert(
'Salvar Avaliação',
`Confirmas a avaliação de ${alunoNome} com nota final de ${notaFinal} valores?`,
`Confirmas a gravação da ficha com nota final de ${notaFinal} valores?`,
[
{ text: 'Cancelar', style: 'cancel' },
{
@@ -209,16 +182,14 @@ export default function FichaAvaliacao() {
onPress: async () => {
setSubmitting(true);
try {
const notaNum = parseFloat(notaFinal.replace(',', '.'));
const { error } = await supabase
.from('avaliacoes_empresa')
.upsert({
estagio_id: params.estagio_id,
aluno_nome: alunoNome,
aluno_n_escola: numEscola && numEscola !== '—' ? parseInt(numEscola, 10) : null,
aluno_n_escola: numEscola !== '—' ? parseInt(numEscola, 10) : null,
aluno_turma: alunoTurma,
nota_final: notaNum,
nota_final: parseFloat(notaFinal.replace(',', '.')),
respostas: respostas,
observacoes: observacoes.trim() || null,
atualizado_em: new Date().toISOString(),
@@ -226,19 +197,10 @@ export default function FichaAvaliacao() {
if (error) throw error;
await supabase
.from('estagios')
.update({ estado: 'Avaliado' })
.eq('id', params.estagio_id);
Alert.alert(
'Avaliação Guardada!',
'A ficha foi gravada com sucesso. O professor poderá agora consultá-la.',
[{ text: 'OK', onPress: () => router.back() }]
);
await supabase.from('estagios').update({ estado: 'Avaliado' }).eq('id', params.estagio_id);
Alert.alert('Sucesso', 'Ficha gravada com sucesso.', [{ text: 'OK', onPress: () => router.back() }]);
} catch (e) {
console.error(e);
Alert.alert('Erro', 'Não foi possível guardar a avaliação. Tenta novamente.');
Alert.alert('Erro', 'Erro ao guardar dados.');
} finally {
setSubmitting(false);
}
@@ -248,34 +210,33 @@ export default function FichaAvaliacao() {
);
};
// ─── Gerar PDF da Avaliação ──────────────────────────────────────────────────
const gerarPDFAvaliacao = async () => {
const gerarPDFAvaliacao = async () => {
if (!podeSometer) {
Alert.alert('Atenção', 'Preenche todas as questões e insere uma nota válida (020) antes de gerar o PDF.');
Alert.alert('Aviso', 'Preenche todos os campos antes de gerar o PDF.');
return;
}
setGerandoPDF(true);
try {
const b64Escola = await getBase64Image(require('../../assets/images/logoepvc3.png'));
const b64App = await getBase64Image(require('../../assets/images/logo_s/texto.png'));
const b64Final = await getBase64Image(require('../../assets/images/logoepvc.png'));
let linhasTabelaHtml = '';
let linhasPerguntas = '';
Object.entries(categorias).forEach(([categoria, perguntas]) => {
linhasPerguntas += `<tr><td colspan="2" class="cat-row">${categoria}</td></tr>`;
perguntas.forEach((p) => {
const valor = respostas[p.id];
const label = ESCALA.find(e => e.valor === valor)?.label ?? '—';
linhasPerguntas += `
<tr>
<td class="td-text">${p.texto}</td>
<td class="td-score"><strong>${valor}</strong> · ${label}</td>
</tr>`;
});
PERGUNTAS.forEach((p, idx) => {
const valor = respostas[p.id];
linhasTabelaHtml += `
<tr>
<td style="text-align:center; border: 1px solid #000; padding: 2px;">${idx + 1}</td>
<td style="text-align:left; border: 1px solid #000; padding: 4px;">${p.texto}</td>
<td style="text-align:center; border: 1px solid #000; font-weight: bold;">${valor === 5 ? 'X' : ''}</td>
<td style="text-align:center; border: 1px solid #000; font-weight: bold;">${valor === 4 ? 'X' : ''}</td>
<td style="text-align:center; border: 1px solid #000; font-weight: bold;">${valor === 3 ? 'X' : ''}</td>
<td style="text-align:center; border: 1px solid #000; font-weight: bold;">${valor === 2 ? 'X' : ''}</td>
<td style="text-align:center; border: 1px solid #000; font-weight: bold;">${valor === 1 ? 'X' : ''}</td>
</tr>
`;
});
const notaNum = parseFloat(notaFinal.replace(',', '.'));
const positiva = notaNum >= 9.5;
const dataHoje = new Date().toLocaleDateString('pt-PT');
// Adicionamos Date.now() para garantir que o nome é sempre único e o sistema não usa cache
const timestamp = Date.now();
const html = `
<!DOCTYPE html>
@@ -283,96 +244,85 @@ export default function FichaAvaliacao() {
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 8mm 10mm; }
html, body { height: 100%; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', sans-serif; color: #1E293B; font-size: 10.5px; position: relative; box-sizing: border-box; }
.header-table { width: 100%; border: none; margin-bottom: 8px; }
.header-table td { border: none; vertical-align: middle; }
h1 { color: #003049; margin: 0; font-size: 16px; }
h2 { color: #F18721; margin: 2px 0 0; font-size: 11px; }
.info { width: 100%; border-collapse: collapse; margin: 8px 0; border: 1px solid #CBD5E1; }
.info td { padding: 4px 8px; border: 1px solid #CBD5E1; font-size: 10.5px; }
.info td.label { background: #F8FAFC; font-weight: bold; width: 25%; }
.section { background: #003049; color: #fff; padding: 4px 8px; font-weight: bold; font-size: 10.5px; margin-top: 10px; }
.tbl { width: 100%; border-collapse: collapse; font-size: 9.5px; }
.tbl td { border: 1px solid #CBD5E1; padding: 3px 6px; }
.cat-row { background: #F18721; color: #fff; font-weight: bold; text-transform: uppercase; font-size: 9px; letter-spacing: 0.5px; padding: 3px 6px; }
.td-text { width: 72%; }
.td-score { width: 28%; text-align: center; font-size: 10px; }
.nota-box { text-align: center; padding: 8px; border: 1px solid #E2E8F0; background: #F8FAFC; margin-top: 6px; }
.nota-num { font-size: 28px; font-weight: 900; color: ${positiva ? '#15803D' : '#B91C1C'}; line-height: 1; }
.nota-lbl { font-size: 11px; color: #64748B; font-weight: bold; margin-top: 2px; }
.obs { border: 1px solid #CBD5E1; padding: 8px; background: #F8FAFC; font-style: italic; min-height: 35px; max-height: 60px; font-size: 10px; line-height: 1.4; overflow: hidden; margin-top: 6px; }
.watermark { position: fixed; top: 30%; left: 15%; opacity: 0.03; z-index: -100; transform: rotate(-30deg); pointer-events: none; }
.footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 8.5px;
color: #64748B;
border-top: 1px solid #E2E8F0;
padding-top: 5px;
}
.footer img { height: 26px; margin-top: 3px; display: inline-block; }
@page { size: A4; margin: 15mm; }
body { font-family: 'Times New Roman', serif; font-size: 10pt; color: #000; }
table { width: 100%; border-collapse: collapse; }
.header-table td { border: none; padding: 2px; }
.main-table th { border: 1px solid #000; padding: 5px; background: #fff; text-align: center; font-size: 9pt; }
.info-row { margin: 15px 0; }
.sig-table { margin-top: 50px; width: 100%; }
.sig-table td { width: 50%; text-align: center; vertical-align: bottom; }
.sig-line { border-top: 1px solid #000; width: 80%; margin: 0 auto; padding-top: 5px; }
</style>
</head>
<body>
<div class="watermark"><img src="${b64Escola}" style="width:500px;" /></div>
<table class="header-table">
<tr>
<td style="width:20%;"><img src="${b64Escola}" style="width:100px;" /></td>
<td style="width:60%; text-align:center;">
<h1>Ficha de Personalidade e Avaliação</h1>
<h2>Formação em Contexto de Trabalho (FCT)</h2>
<td style="text-align: left; font-family: monospace;">EPVC.FI.08/1</td>
<td></td>
<td style="text-align: right;">Aprovado: 01/09/2016</td>
</tr>
<tr>
<td colspan="3" style="text-align: center; padding: 20px 0;">
<h2 style="margin: 0; font-size: 14pt;">Escola Profissional de Vila do Conde</h2>
<h3 style="margin: 5px 0; font-size: 11pt; font-weight: normal;">CURSO PROFISSIONAL TÉCNICO/A DE INFORMÁTICA DE GESTÃO</h3>
<h3 style="margin: 5px 0; font-size: 12pt; text-decoration: underline;">FICHA DE AVALIAÇÃO FINAL DO ESTÁGIO</h3>
</td>
<td style="width:20%; text-align:right;"><img src="${b64App}" style="width:75px;" /></td>
</tr>
</table>
<table class="info">
<tr><td class="label">Estagiário(a):</td><td><strong>${alunoNome}</strong></td></tr>
<tr><td class="label">Nº Escola / Turma:</td><td>${numEscola} &bull; ${alunoTurma}</td></tr>
<tr><td class="label">Data de Emissão:</td><td>${new Date().toLocaleDateString('pt-PT')}</td></tr>
<div class="info-row">
<strong>Estagiário:</strong> ${alunoNome} &nbsp;&nbsp;&nbsp; <strong>Ano Letivo:</strong> 2025/2026<br><br>
<strong>Instituição:</strong> __________________________________________________________________________
</div>
<table class="main-table">
<thead>
<tr>
<th style="width: 5%;">#</th>
<th style="width: 55%;">Parâmetros de Avaliação</th>
<th style="width: 8%;">5</th>
<th style="width: 8%;">4</th>
<th style="width: 8%;">3</th>
<th style="width: 8%;">2</th>
<th style="width: 8%;">1</th>
</tr>
</thead>
<tbody>
${linhasTabelaHtml}
</tbody>
</table>
<div class="section">Avaliação Qualitativa (1 = Insatisfatório &bull; 5 = Excelente)</div>
<table class="tbl">${linhasPerguntas}</table>
<p style="text-align: justify; margin-top: 20px;">
Pelas competências demonstradas durante o estágio de Formação em Contexto de Trabalho, propomos que seja atribuída ao estagiário a classificação de <strong>${notaFinal}</strong> valores.
</p>
<div class="section">Classificação Final</div>
<div class="nota-box">
<div class="nota-num">${notaFinal}</div>
<div class="nota-lbl">Valores &bull; ${positiva ? 'APROVADO' : 'REPROVADO'}</div>
</div>
<p><strong>Observações:</strong></p>
<div style="border: 1px solid #000; min-height: 40px; padding: 5px;">${observacoes}</div>
<div class="section">Observações da Entidade Acolhedora</div>
<div class="obs">${observacoes.trim() ? observacoes.replace(/\n/g, '<br/>') : '<em>Sem observações adicionais registadas.</em>'}</div>
<p style="margin-top: 30px; text-align: right;">
Escola Profissional de Vila do Conde, ${dataHoje}
</p>
<div class="footer">
Documento gerado digitalmente via plataforma Estágios+ EPVC &bull; ${new Date().toLocaleDateString('pt-PT')}<br/>
<img src="${b64Final}" />
</div>
<table class="sig-table">
<tr>
<td><div class="sig-line">O Formador acompanhante</div></td>
<td><div class="sig-line">O Monitor de estágio</div></td>
</tr>
</table>
</body>
</html>`;
const { uri } = await Print.printToFileAsync({ html });
const safeName = (alunoNome || 'aluno').replace(/[^a-zA-Z0-9]/g, '_');
const newUri = `${FileSystem.documentDirectory}Avaliacao_Empresa_${safeName}.pdf`;
await FileSystem.moveAsync({ from: uri, to: newUri });
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(newUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' });
}
// O truque está aqui: incluímos o timestamp no nome do ficheiro final
const safeName = alunoNome.replace(/\s/g, '_');
const finalPath = `${FileSystem.documentDirectory}Avaliacao_${safeName}_${timestamp}.pdf`;
await FileSystem.moveAsync({ from: uri, to: finalPath });
await Sharing.shareAsync(finalPath);
} catch (e) {
console.error(e);
Alert.alert('Erro', 'Não foi possível gerar o PDF da avaliação.');
Alert.alert('Erro', 'Falha ao gerar o documento.');
} finally {
setGerandoPDF(false);
}
@@ -380,9 +330,8 @@ export default function FichaAvaliacao() {
if (loadingDados) {
return (
<View style={[s.loadingContainer, { backgroundColor: cores.fundo }]}>
<View style={[s.loadingBox, { backgroundColor: cores.fundo }]}>
<ActivityIndicator size="large" color={cores.azulMarinho} />
<Text style={[s.loadingText, { color: cores.textoSecundario }]}>A carregar avaliação existente...</Text>
</View>
);
}
@@ -392,264 +341,139 @@ export default function FichaAvaliacao() {
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={s.header}>
<TouchableOpacity onPress={() => router.back()} style={s.btnBack}>
<TouchableOpacity onPress={() => router.back()} style={s.btnVoltar}>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<View style={{ flex: 1 }}>
<View>
<Text style={[s.headerTitle, { color: cores.texto }]}>Ficha de Avaliação</Text>
<Text style={[s.headerSub, { color: cores.textoSecundario }]}>Formação em Contexto de Trabalho</Text>
<Text style={[s.headerSub, { color: cores.textoSecundario }]}>Preenchimento da Grelha Oficial</Text>
</View>
</View>
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<ScrollView contentContainerStyle={s.scroll} showsVerticalScrollIndicator={false}>
<ScrollView contentContainerStyle={s.container} showsVerticalScrollIndicator={false}>
<View style={[s.alunoCard, { backgroundColor: cores.azulMarinho }]}>
<View style={s.alunoAvatar}>
<Text style={s.alunoAvatarLetra}>{alunoNome?.charAt(0) ?? '?'}</Text>
<View style={[s.cardAluno, { backgroundColor: cores.azulMarinho }]}>
<View style={s.avatar}>
<Text style={s.avatarLetra}>{alunoNome.charAt(0).toUpperCase()}</Text>
</View>
<View style={{ flex: 1, marginLeft: 14 }}>
<Text style={s.alunoNome}>{alunoNome}</Text>
<Text style={s.alunoMeta}> {numEscola} · {alunoTurma}</Text>
</View>
<View style={[s.badgeFCT, { backgroundColor: cores.laranja }]}>
<Text style={s.badgeFCTText}>FCT</Text>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={s.nomeAluno}>{alunoNome}</Text>
<Text style={s.metaAluno}> {numEscola} Turma {alunoTurma}</Text>
</View>
</View>
<View style={[s.instrucoes, { backgroundColor: cores.verdeAgua + '22', borderColor: cores.verdeAgua }]}>
<Ionicons name="information-circle-outline" size={18} color={cores.verdeAgua} style={{ marginRight: 8, marginTop: 1 }} />
<Text style={[s.instrucoesTxt, { color: cores.texto }]}>
Avalia cada item de <Text style={{ fontWeight: '900' }}>1 (Insatisfatório)</Text> a <Text style={{ fontWeight: '900' }}>5 (Excelente)</Text>. No final, atribui uma nota de 0 a 20.
</Text>
</View>
{PERGUNTAS.map((p, index) => {
const atual = respostas[p.id];
return (
<View key={p.id} style={[s.cardPergunta, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Text style={[s.txtPergunta, { color: cores.texto }]}>
<Text style={{ color: cores.laranja, fontWeight: 'bold' }}>{index + 1}.</Text> {p.texto}
</Text>
{Object.entries(categorias).map(([categoria, perguntas], catIdx) => (
<View key={catIdx}>
<View style={[s.catHeader, { backgroundColor: cores.azulMarinho }]}>
<Text style={s.catHeaderTxt}>{categoria}</Text>
<View style={s.rowEscala}>
{COLUNAS_ESCALA.map(col => {
const ativo = atual === col.valor;
return (
<TouchableOpacity
key={col.valor}
style={[
s.btnEscala,
{
backgroundColor: ativo ? cores.laranja : cores.inputFundo,
borderColor: ativo ? cores.laranja : cores.borda
}
]}
onPress={() => setRespostas(pvs => ({ ...pvs, [p.id]: col.valor }))}
>
<Text style={[s.txtEscalaNum, { color: ativo ? '#fff' : cores.texto }]}>{col.valor}</Text>
<Text style={[s.txtEscalaLabel, { color: ativo ? '#fff' : cores.textoSecundario }]}>{col.label}</Text>
</TouchableOpacity>
);
})}
</View>
</View>
);
})}
{perguntas.map((pergunta) => {
const selecionado = respostas[pergunta.id];
const globalIdx = PERGUNTAS.findIndex(p => p.id === pergunta.id) + 1;
return (
<View key={pergunta.id} style={[s.perguntaCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={s.perguntaTop}>
<View style={[s.numCircle, { backgroundColor: selecionado ? cores.laranja : cores.borda }]}>
<Text style={[s.numTxt, { color: selecionado ? '#fff' : cores.textoSecundario }]}>{globalIdx}</Text>
</View>
<Text style={[s.perguntaTxt, { color: cores.texto }]}>{pergunta.texto}</Text>
</View>
<View style={s.escalaRow}>
{ESCALA.map(e => {
const esteSelecionado = selecionado === e.valor;
const cor = corEscala(e.valor, esteSelecionado);
return (
<TouchableOpacity
key={e.valor}
style={[s.escalaBotao, { backgroundColor: cor.bg, borderColor: cor.borda }]}
onPress={() => setRespostas(prev => ({ ...prev, [pergunta.id]: e.valor }))}
activeOpacity={0.75}
>
<Text style={[s.escalaNum, { color: cor.texto }]}>{e.valor}</Text>
</TouchableOpacity>
);
})}
</View>
{selecionado && (
<Text style={[s.escalaLabel, { color: cores.textoSecundario }]}>
{ESCALA.find(e => e.valor === selecionado)?.label}
</Text>
)}
</View>
);
})}
</View>
))}
<View style={[s.secaoFinal, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Text style={[s.secaoTitulo, { color: cores.texto }]}>Nota Final</Text>
<Text style={[s.secaoDesc, { color: cores.textoSecundario }]}>
Insira a nota obtida na escala de <Text style={{ fontWeight: '800' }}>0 a 20 valores</Text>.
</Text>
<View style={s.notaRow}>
<View style={[s.cardSeccao, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Text style={[s.tituloSeccao, { color: cores.texto }]}>Classificação Final (0 a 20)</Text>
<View style={s.rowNota}>
<TextInput
style={[
s.notaInput,
{
backgroundColor: cores.inputFundo,
borderColor: notaFinal === '' ? cores.borda : notaValida ? cores.verde : cores.vermelho,
color: cores.texto,
},
]}
placeholder="0-20"
placeholderTextColor={cores.textoSecundario}
style={[s.inputNota, { backgroundColor: cores.inputFundo, borderColor: cores.borda, color: cores.texto }]}
keyboardType="decimal-pad"
placeholder="20"
placeholderTextColor={cores.textoSecundario}
value={notaFinal}
onChangeText={setNotaFinal}
maxLength={4}
/>
<Text style={[s.notaSufixo, { color: cores.textoSecundario }]}>valores</Text>
{notaFinal !== '' && (
<View style={[
s.notaBadge,
{ backgroundColor: notaValida ? (parseFloat(notaFinal.replace(',', '.')) >= 9.5 ? '#DCFCE7' : '#FEE2E2') : '#F3F4F6' }
]}>
<Text style={[
s.notaBadgeTxt,
{ color: notaValida ? (parseFloat(notaFinal.replace(',', '.')) >= 9.5 ? '#15803D' : '#B91C1C') : cores.textoSecundario }
]}>
{notaValida
? parseFloat(notaFinal.replace(',', '.')) >= 9.5 ? '✓ Positiva' : '✗ Negativa'
: 'Inválido'}
</Text>
</View>
)}
<Text style={[s.sufixoValores, { color: cores.textoSecundario }]}>valores</Text>
</View>
</View>
<View style={[s.secaoFinal, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Text style={[s.secaoTitulo, { color: cores.texto }]}>Observações</Text>
<Text style={[s.secaoDesc, { color: cores.textoSecundario }]}>Opcional comentários adicionais sobre o desempenho.</Text>
<View style={[s.cardSeccao, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Text style={[s.tituloSeccao, { color: cores.texto }]}>Observações Adicionais</Text>
<TextInput
style={[s.obsInput, { backgroundColor: cores.inputFundo, borderColor: cores.borda, color: cores.texto }]}
placeholder="Escreve aqui as tuas observações..."
placeholderTextColor={cores.textoSecundario}
style={[s.inputObs, { backgroundColor: cores.inputFundo, borderColor: cores.borda, color: cores.texto }]}
multiline
numberOfLines={4}
placeholder="Notas sobre o comportamento ou competências técnicas demonstradas..."
placeholderTextColor={cores.textoSecundario}
value={observacoes}
onChangeText={setObservacoes}
textAlignVertical="top"
/>
</View>
<View style={[s.progressoBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={s.progressoRow}>
<Text style={[s.progressoLabel, { color: cores.textoSecundario }]}>
{Object.keys(respostas).length} de {PERGUNTAS.length} questões respondidas
</Text>
<Text style={[s.progressoLabel, { color: todasRespondidas ? cores.verde : cores.laranja, fontWeight: '800' }]}>
{todasRespondidas ? '✓ Completo' : 'Incompleto'}
</Text>
</View>
<View style={[s.progressoBar, { backgroundColor: cores.borda }]}>
<View style={[
s.progressoFill,
{
backgroundColor: todasRespondidas ? cores.verde : cores.laranja,
width: `${(Object.keys(respostas).length / PERGUNTAS.length) * 100}%`
}
]} />
</View>
</View>
<View style={s.botoesRow}>
<View style={s.botoesContainer}>
<TouchableOpacity
style={[s.btnSalvar, { backgroundColor: podeSometer ? cores.azulMarinho : cores.borda }]}
onPress={handleSubmit}
style={[s.btnAcao, { backgroundColor: podeSometer ? cores.azulMarinho : cores.borda }]}
disabled={!podeSometer || submitting}
activeOpacity={0.85}
onPress={handleSubmit}
>
{submitting ? (
<ActivityIndicator color="#fff" />
) : (
<>
<Ionicons name="save" size={20} color={podeSometer ? '#fff' : cores.textoSecundario} />
<Text style={[s.btnTxt, { color: podeSometer ? '#fff' : cores.textoSecundario }]}>
Salvar Avaliação
</Text>
</>
)}
{submitting ? <ActivityIndicator color="#fff" /> : <Text style={s.btnAcaoTxt}>Gravar Avaliação</Text>}
</TouchableOpacity>
<TouchableOpacity
style={[s.btnPDF, { backgroundColor: podeSometer ? cores.laranja : cores.borda }]}
onPress={gerarPDFAvaliacao}
style={[s.btnAcao, { backgroundColor: podeSometer ? cores.laranja : cores.borda }]}
disabled={!podeSometer || gerandoPDF}
activeOpacity={0.85}
onPress={gerarPDFAvaliacao}
>
{gerandoPDF ? (
<ActivityIndicator color="#fff" />
) : (
<>
<Ionicons name="document-text" size={20} color={podeSometer ? '#fff' : cores.textoSecundario} />
<Text style={[s.btnTxt, { color: podeSometer ? '#fff' : cores.textoSecundario }]}>
Exportar PDF
</Text>
</>
)}
{gerandoPDF ? <ActivityIndicator color="#fff" /> : <Text style={s.btnAcaoTxt}>Gerar PDF Oficial</Text>}
</TouchableOpacity>
</View>
<View style={{ height: 30 }} />
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
// ─── Estilos Customizados ──────────────────────────────────────────────────────
const s = StyleSheet.create({
safe: { flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingTop: 15, paddingBottom: 12, gap: 14 },
btnBack: { padding: 4 },
headerTitle: { fontSize: 18, fontWeight: '900' },
headerSub: { fontSize: 12, fontWeight: '500', marginTop: 1 },
scroll: { paddingHorizontal: 16, paddingBottom: 20 },
loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 },
loadingText: { fontSize: 14, fontWeight: '600' },
alunoCard: { flexDirection: 'row', alignItems: 'center', borderRadius: 18, padding: 18, marginBottom: 16 },
alunoAvatar: { width: 46, height: 46, borderRadius: 23, backgroundColor: 'rgba(255,255,255,0.2)', justifyContent: 'center', alignItems: 'center' },
alunoAvatarLetra: { color: '#fff', fontSize: 20, fontWeight: '900' },
alunoNome: { color: '#fff', fontSize: 16, fontWeight: '900' },
alunoMeta: { color: 'rgba(255,255,255,0.7)', fontSize: 12, fontWeight: '600', marginTop: 3 },
badgeFCT: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8 },
badgeFCTText: { color: '#fff', fontSize: 11, fontWeight: '900', letterSpacing: 1 },
instrucoes: { flexDirection: 'row', alignItems: 'flex-start', borderRadius: 12, borderWidth: 1, padding: 12, marginBottom: 20 },
instrucoesTxt: { flex: 1, fontSize: 13, lineHeight: 19 },
catHeader: { borderRadius: 8, paddingVertical: 7, paddingHorizontal: 14, marginBottom: 10, marginTop: 6 },
catHeaderTxt: { color: '#fff', fontSize: 11, fontWeight: '800', letterSpacing: 1, textTransform: 'uppercase' },
perguntaCard: { borderRadius: 16, borderWidth: 1, padding: 16, marginBottom: 10 },
perguntaTop: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 14, gap: 12 },
numCircle: { width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', flexShrink: 0, marginTop: 1 },
numTxt: { fontSize: 13, fontWeight: '900' },
perguntaTxt: { flex: 1, fontSize: 14, fontWeight: '600', lineHeight: 20 },
escalaRow: { flexDirection: 'row', gap: 8, justifyContent: 'space-between' },
escalaBotao: { flex: 1, aspectRatio: 1, borderRadius: 12, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center', maxWidth: 54 },
escalaNum: { fontSize: 17, fontWeight: '900' },
escalaLabel: { fontSize: 11, fontWeight: '600', marginTop: 8, textAlign: 'center' },
secaoFinal: { borderRadius: 18, borderWidth: 1, padding: 18, marginBottom: 14 },
secaoTitulo: { fontSize: 16, fontWeight: '900', marginBottom: 4 },
secaoDesc: { fontSize: 13, marginBottom: 14, lineHeight: 18 },
notaRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
notaInput: { borderWidth: 2, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, fontSize: 22, fontWeight: '900', width: 95, textAlign: 'center' },
notaSufixo: { fontSize: 15, fontWeight: '700' },
notaBadge: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8 },
notaBadgeTxt: { fontSize: 13, fontWeight: '800' },
obsInput: { borderWidth: 1.5, borderRadius: 12, padding: 14, fontSize: 14, minHeight: 100, lineHeight: 21 },
progressoBox: { borderRadius: 14, borderWidth: 1, padding: 14, marginBottom: 16 },
progressoRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 },
progressoLabel: { fontSize: 12, fontWeight: '600' },
progressoBar: { height: 6, borderRadius: 3, overflow: 'hidden' },
progressoFill: { height: '100%', borderRadius: 3 },
botoesRow: { flexDirection: 'row', gap: 10, marginTop: 4 },
btnSalvar: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 16, borderRadius: 18 },
btnPDF: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 16, borderRadius: 18 },
btnTxt: { fontSize: 14, fontWeight: '900' },
safe: { flex: 1 },
loadingBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
header: { flexDirection: 'row', alignItems: 'center', padding: 16, gap: 12 },
btnVoltar: { padding: 4 },
headerTitle: { fontSize: 18, fontWeight: 'bold' },
headerSub: { fontSize: 12, marginTop: 2 },
container: { padding: 16, gap: 14, paddingBottom: 40 },
cardAluno: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 12 },
avatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)', justifyContent: 'center', alignItems: 'center' },
avatarLetra: { color: '#fff', fontSize: 18, fontWeight: 'bold' },
nomeAluno: { color: '#fff', fontSize: 15, fontWeight: 'bold' },
metaAluno: { color: 'rgba(255,255,255,0.7)', fontSize: 12, marginTop: 2 },
cardPergunta: { padding: 16, borderRadius: 12, borderWidth: 1, gap: 12 },
txtPergunta: { fontSize: 14, fontWeight: '500', lineHeight: 20 },
rowEscala: { flexDirection: 'row', gap: 6 },
btnEscala: { flex: 1, paddingVertical: 8, borderRadius: 8, borderWidth: 1, alignItems: 'center', justifyContent: 'center' },
txtEscalaNum: { fontSize: 15, fontWeight: 'bold' },
txtEscalaLabel: { fontSize: 8, marginTop: 2, textAlign: 'center' },
cardSeccao: { padding: 16, borderRadius: 12, borderWidth: 1 },
tituloSeccao: { fontSize: 14, fontWeight: 'bold', marginBottom: 10 },
rowNota: { flexDirection: 'row', alignItems: 'center', gap: 12 },
inputNota: { width: 80, padding: 12, borderRadius: 8, borderWidth: 1, fontSize: 16, fontWeight: 'bold', textAlign: 'center' },
sufixoValores: { fontSize: 14, fontWeight: '500' },
inputObs: { minHeight: 80, borderRadius: 8, borderWidth: 1, padding: 12, fontSize: 14, textAlignVertical: 'top' },
botoesContainer: { flexDirection: 'row', gap: 10, marginTop: 10 },
btnAcao: { flex: 1, height: 50, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
btnAcaoTxt: { color: '#fff', fontSize: 14, fontWeight: 'bold' },
});

View File

@@ -161,24 +161,31 @@ export default function GestaoRelatorios() {
}, [relatorios, turmaAtiva]);
// ─── Cabeçalho/Rodapé/Marca de água partilhados ────────────────────────────
const gerarCabecalhoERodape = async () => {
const logoEscola = require('../../assets/logoepvc3.png');
const logoEstagiosPlus = require('../../assets/logo_estagios_plus.png');
const logoRodape = require('../../assets/logoepvc.png');
const gerarCabecalhoERodape = async () => {
// CORREÇÃO: Importamos o módulo completo e guardamos numa constante limpa
const reactNative = require('react-native');
const RNImage = reactNative.Image;
// Agora a resolução dos caminhos vai funcionar sem erros de sintaxe
const logoEscolaUri = RNImage.resolveAssetSource(require('../../../assets/images/logoepvc2.png')).uri;
const logoAppUri = RNImage.resolveAssetSource(require('../../../assets/images/logo.png')).uri;
const logoRodapeUri = RNImage.resolveAssetSource(require('../../../assets/images/logoepvc.png')).uri;
const header = `
<div style="width: 100%; border-bottom: 2px solid #003049; padding-bottom: 10px; margin-bottom: 20px; font-family: sans-serif;">
<div style="width: 100%; border-bottom: 2px solid #003049; padding-bottom: 12px; margin-bottom: 20px; font-family: sans-serif;">
<table style="width: 100%; border-collapse: collapse; border: none;">
<tr>
<td style="width: 30%; border: none; padding: 0; vertical-align: middle;">
<img src="${logoEscola}" style="height: 42px; max-width: 100%; object-fit: contain;" />
<td style="width: 30%; border: none; padding: 0; vertical-align: middle; text-align: left;">
<img src="${logoEscolaUri}" style="height: 38px; max-height: 38px; width: auto; object-fit: contain;" />
</td>
<td id="pdf-title-container" style="width: 45%; border: none; padding: 0 10px; text-align: center; vertical-align: middle;">
<td id="pdf-title-container" style="width: 40%; border: none; padding: 0 10px; text-align: center; vertical-align: middle;">
</td>
<td style="width: 25%; border: none; padding: 0; text-align: right; vertical-align: middle;">
<div style="font-size: 7px; color: #64748B; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: bold;">Plataforma Oficial</div>
<img src="${logoEstagiosPlus}" style="height: 32px; max-width: 100%; object-fit: contain;" />
<td style="width: 30%; border: none; padding: 0; text-align: right; vertical-align: middle;">
<img src="${logoAppUri}" style="height: 38px; max-height: 38px; width: auto; object-fit: contain;" />
</td>
</tr>
</table>
@@ -190,8 +197,8 @@ const logoRodape = require('../../assets/logoepvc.png');
<table style="width: 100%; border-collapse: collapse; border: none;">
<tr>
<td style="text-align: left; border: none; color: #64748B;">Documento gerado automaticamente pela Plataforma Estágios+</td>
<td style="text-align: right; border: none;">
<img src="${logoRodape}" style="height: 20px; vertical-align: middle; margin-left: 5px;" />
<td style="text-align: right; border: none; vertical-align: middle;">
<img src="${logoRodapeUri}" style="height: 20px; max-height: 20px; width: auto; object-fit: contain; vertical-align: middle;" />
</td>
</tr>
</table>
@@ -200,7 +207,7 @@ const logoRodape = require('../../assets/logoepvc.png');
const watermark = `
<div style="position: absolute; top: 38%; left: 20%; width: 60%; opacity: 0.03; z-index: -1000; text-align: center;">
<img src="${logoEstagiosPlus}" style="width: 100%; max-width: 350px;" />
<img src="${logoAppUri}" style="width: 100%; max-width: 350px;" />
</div>
`;