update - apresentação
This commit is contained in:
@@ -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 (0–20).');
|
||||
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 (0–20) 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} • ${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} <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 • 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 • ${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 • ${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}>Nº {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}>Nº {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' },
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user