This commit is contained in:
2026-05-24 19:04:33 +01:00
parent 5f719f033b
commit 34fea77545
4 changed files with 906 additions and 1503 deletions

View File

@@ -10,6 +10,7 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
Image,
Linking,
Modal,
Platform,
@@ -40,7 +41,7 @@ const getFeriadosMap = (ano: number) => ({
[`${ano}-01-01`]: "Ano Novo", [`${ano}-04-25`]: "Dia da Liberdade",
[`${ano}-05-01`]: "Dia do Trabalhador", [`${ano}-06-10`]: "Dia de Portugal",
[`${ano}-06-24`]: "São João (Vila do Conde)", [`${ano}-08-15`]: "Assunção de Nª Senhora",
[`${ano}-10-05`]: "Impl আমের da República", [`${ano}-11-01`]: "Todos os Santos",
[`${ano}-10-05`]: "Implantação da República", [`${ano}-11-01`]: "Todos os Santos",
[`${ano}-12-01`]: "Restauração da Independência", [`${ano}-12-08`]: "Imaculada Conceição",
[`${ano}-12-25`]: "Natal"
});
@@ -391,10 +392,9 @@ const AlunoHome = memo(() => {
<Ionicons name="calendar-outline" size={28} color={themeStyles.textoSecundario} />
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: themeStyles.textoSecundario }}>Data fora do período de estágio</Text>
</View>
);
);
}
// 🟢 Correção: MOVIDO PARA CIMA para garantir que mostra em QUALQUER fim de semana ou feriado sem permissão
if (infoData.bloqueadoPorRegra) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: themeStyles.card, padding: 16, borderRadius: 16, marginTop: 15, borderWidth: 1, borderColor: themeStyles.borda }}>
@@ -404,7 +404,7 @@ const AlunoHome = memo(() => {
);
}
if (selectedDate > hojeStr) return null; // Apenas dias ÚTEIS no futuro ficam escondidos
if (selectedDate > hojeStr) return null;
let config = { icon: 'information-circle', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Nenhum Registo Efetuado' };
return (
@@ -472,7 +472,21 @@ const AlunoHome = memo(() => {
<ScrollView ref={scrollViewRef} contentContainerStyle={styles.container} showsVerticalScrollIndicator={false} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />}>
<View style={styles.topBar}>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
{/* 🟢 IMAGEM COM SCALE 1.9 PARA FICAR MAIOR 🟢 */}
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Image
source={require('../../assets/images/logo_s/texto.png')}
style={{
width: 45,
height: 45,
resizeMode: 'contain',
marginRight: 10,
transform: [{ scale: 2.5 }]
}}
/>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
</View>
<View style={styles.topIcons}>
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')} style={{ marginRight: 15 }}><Ionicons name="settings-outline" size={26} color={themeStyles.texto} /></TouchableOpacity>
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}><Ionicons name="person-circle-outline" size={30} color={themeStyles.texto} /></TouchableOpacity>
@@ -610,6 +624,34 @@ const AlunoHome = memo(() => {
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_fim}</Text>
</View>
</View>
{/* BOTÃO DA AUTOAVALIAÇÃO NA TAB INFO */}
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<TouchableOpacity
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: themeStyles.laranja + '15',
padding: 15,
borderRadius: 15,
marginTop: 5
}}
onPress={() => router.push('/Aluno/autoavaliacao')}
activeOpacity={0.7}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
<View style={{ backgroundColor: themeStyles.card, padding: 8, borderRadius: 10 }}>
<Ionicons name="star" size={20} color={themeStyles.laranja} />
</View>
<View>
<Text style={{ fontSize: 14, fontWeight: '800', color: themeStyles.texto }}>Ficha de Autoavaliação</Text>
<Text style={{ fontSize: 11, fontWeight: '600', color: themeStyles.textoSecundario }}>Avalia o teu local de estágio</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={20} color={themeStyles.laranja} />
</TouchableOpacity>
</View>
)}
</View>

226
app/Aluno/autoavaliacao.tsx Normal file
View File

@@ -0,0 +1,226 @@
// app/Aluno/autoavaliacao.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../../lib/supabase';
import { useTheme } from '../../themecontext';
export default function AutoavaliacaoAluno() {
const { isDarkMode } = useTheme();
const router = useRouter();
const [loading, setLoading] = useState(true);
const [submetendo, setSubmetendo] = useState(false);
const [estagioId, setEstagioId] = useState<string | null>(null);
const [alunoId, setAlunoId] = useState<string | null>(null);
const [jaPreencheu, setJaPreencheu] = useState(false);
// States dos Critérios (1 a 5)
const [assiduidade, setAssiduidade] = useState(0);
const [responsabilidade, setResponsabilidade] = useState(0);
const [relacionamento, setRelacionamento] = useState(0);
const [conhecimentos, setConhecimentos] = useState(0);
const [nota, setNota] = useState('');
const [comentarios, setComentarios] = useState('');
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]);
useEffect(() => { carregarDados(); }, []);
const carregarDados = async () => {
try {
const { data: userData, error: userError } = await supabase.auth.getUser();
if (userError || !userData?.user) throw new Error('Utilizador não autenticado.');
const currentUserId = userData.user.id;
setAlunoId(currentUserId);
const { data: estagioData, error: estagioError } = await supabase
.from('estagios').select('id').eq('aluno_id', currentUserId).order('data_inicio', { ascending: false }).limit(1).single();
if (estagioError && estagioError.code !== 'PGRST116') throw estagioError;
if (estagioData) {
setEstagioId(estagioData.id);
const { data: avaliacaoData } = await supabase.from('autoavaliacoes').select('id').eq('estagio_id', estagioData.id).single();
if (avaliacaoData) setJaPreencheu(true);
}
} catch (error) {
console.error(error);
Alert.alert('Erro', 'Não foi possível carregar os dados.');
} finally {
setLoading(false);
}
};
const submeterAvaliacao = async () => {
if (assiduidade === 0 || responsabilidade === 0 || relacionamento === 0 || conhecimentos === 0) {
return Alert.alert('Atenção', 'Por favor, seleciona um número de 1 a 5 para cada critério.');
}
if (!nota || nota.trim() === '') {
return Alert.alert('Atenção', 'Por favor, insere a nota final que propões (0 a 20).');
}
const notaNum = parseInt(nota);
if (isNaN(notaNum) || notaNum < 0 || notaNum > 20) {
return Alert.alert('Nota Inválida', 'A nota tem de ser um número inteiro entre 0 e 20.');
}
setSubmetendo(true);
try {
const { error } = await supabase.from('autoavaliacoes').insert({
estagio_id: estagioId,
aluno_id: alunoId,
assiduidade,
responsabilidade,
relacionamento,
conhecimentos,
nota_desejada: notaNum,
comentarios
});
if (error) throw error;
Alert.alert('Sucesso', 'A tua autoavaliação foi submetida!');
setJaPreencheu(true);
} catch (error) {
console.error(error);
Alert.alert('Erro', 'Ocorreu um erro ao submeter.');
} finally {
setSubmetendo(false);
}
};
// 🟢 NOVA FUNÇÃO PARA RENDERIZAR NÚMEROS (Substitui as estrelas)
const renderNumeros = (valor: number, setValor: (v: number) => void) => (
<View style={styles.numerosContainer}>
{[1, 2, 3, 4, 5].map((num) => (
<TouchableOpacity
key={num}
style={[styles.numBtn, valor === num && { backgroundColor: cores.laranja }]}
onPress={() => setValor(num)}
>
<Text style={[styles.numText, { color: valor === num ? '#FFF' : cores.texto }]}>{num}</Text>
</TouchableOpacity>
))}
</View>
);
if (loading) return <View style={[styles.safeArea, { backgroundColor: cores.fundo, justifyContent: 'center' }]}><ActivityIndicator size="large" color={cores.azulMarinho} /></View>;
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
<View style={[styles.header, { paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 15 : 35 }]}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => router.back()}><Ionicons name="arrow-back" size={24} color={cores.texto} /></TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Autoavaliação</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.introBox}>
<Ionicons name="clipboard-outline" size={32} color={cores.laranja} />
<Text style={[styles.introText, { color: cores.texto }]}>Avalia o teu desempenho (1 a 5) e propõe a tua nota final (0 a 20).</Text>
</View>
{jaPreencheu ? (
<View style={[styles.sucessoBox, { backgroundColor: cores.verdeAgua + '20', borderColor: cores.verdeAgua }]}>
<Ionicons name="checkmark-circle" size={40} color={cores.verdeAgua} />
<Text style={[styles.sucessoText, { color: cores.texto }]}> submeteste a tua autoavaliação.</Text>
</View>
) : (
<View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.criterioBox}>
<Text style={[styles.criterioLabel, { color: cores.texto }]}>Assiduidade e Pontualidade</Text>
{renderNumeros(assiduidade, setAssiduidade)}
</View>
<View style={styles.criterioBox}>
<Text style={[styles.criterioLabel, { color: cores.texto }]}>Responsabilidade e Iniciativa</Text>
{renderNumeros(responsabilidade, setResponsabilidade)}
</View>
<View style={styles.criterioBox}>
<Text style={[styles.criterioLabel, { color: cores.texto }]}>Relacionamento Interpessoal</Text>
{renderNumeros(relacionamento, setRelacionamento)}
</View>
<View style={[styles.criterioBox, { borderBottomWidth: 0, paddingBottom: 0, marginBottom: 0 }]}>
<Text style={[styles.criterioLabel, { color: cores.texto }]}>Aplicação de Conhecimentos</Text>
{renderNumeros(conhecimentos, setConhecimentos)}
</View>
</View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, alignItems: 'center', paddingVertical: 30 }]}>
<Text style={[styles.criterioLabel, { color: cores.texto, textAlign: 'center', marginBottom: 15 }]}>Nota Final Proposta (0 a 20)</Text>
<View style={{ flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'center' }}>
<TextInput
style={[styles.notaInput, { color: cores.azulMarinho, borderBottomColor: cores.borda }]}
keyboardType="number-pad" maxLength={2} value={nota} onChangeText={(val) => setNota(val.replace(/[^0-9]/g, ''))}
placeholder="--" placeholderTextColor={cores.textoSecundario + '50'}
/>
<Text style={{ fontSize: 24, fontWeight: '800', color: cores.textoSecundario, marginBottom: 5, marginLeft: 5 }}>Val.</Text>
</View>
</View>
<TextInput
style={[styles.inputArea, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
placeholder="Comentários adicionais..." placeholderTextColor={cores.textoSecundario}
multiline numberOfLines={5} value={comentarios} onChangeText={setComentarios} textAlignVertical="top"
/>
<TouchableOpacity style={[styles.btnSubmeter, { backgroundColor: cores.laranja }]} onPress={submeterAvaliacao} disabled={submetendo}>
{submetendo ? <ActivityIndicator color="#FFF" /> : <><Ionicons name="send" size={20} color="#FFF" /><Text style={styles.btnSubmeterText}>Submeter Avaliação</Text></>}
</TouchableOpacity>
</View>
)}
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 15 },
btnVoltar: { padding: 5, marginLeft: -5 },
headerTitle: { fontSize: 18, fontWeight: '900' },
scrollContent: { padding: 20, paddingBottom: 40 },
introBox: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#F1872115', padding: 15, borderRadius: 15, marginBottom: 25, gap: 12 },
introText: { flex: 1, fontSize: 13, fontWeight: '600', lineHeight: 18 },
card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 15 },
criterioBox: { borderBottomWidth: 1, borderBottomColor: '#E2E8F0', paddingBottom: 15, marginBottom: 15 },
criterioLabel: { fontSize: 15, fontWeight: '800' },
// Estilo dos botões numéricos
numerosContainer: { flexDirection: 'row', gap: 10, marginTop: 12 },
numBtn: { width: 45, height: 45, borderRadius: 12, backgroundColor: '#E2E8F0', justifyContent: 'center', alignItems: 'center' },
numText: { fontSize: 16, fontWeight: '900' },
notaInput: { fontSize: 50, fontWeight: '900', textAlign: 'center', width: 80, borderBottomWidth: 3, paddingBottom: 0 },
inputArea: { borderRadius: 12, borderWidth: 1, padding: 15, fontSize: 14, minHeight: 120, marginBottom: 15 },
btnSubmeter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 18, borderRadius: 15, marginTop: 10, gap: 10 },
btnSubmeterText: { color: '#FFF', fontSize: 15, fontWeight: 'bold' },
sucessoBox: { padding: 25, borderRadius: 20, borderWidth: 1, alignItems: 'center', marginTop: 20 },
sucessoText: { fontSize: 14, fontWeight: '600', textAlign: 'center', marginTop: 15, lineHeight: 22 }
});

View File

@@ -46,7 +46,6 @@ export default function GestaoRelatorios() {
laranja: '#F18721',
}), [isDarkMode]);
// 🟢 LÊ AS IMAGENS DIRETAMENTE DA TUA APP (OFFLINE)
const getBase64Image = async (imageModule: any) => {
try {
const asset = Asset.fromModule(imageModule);
@@ -74,7 +73,8 @@ export default function GestaoRelatorios() {
.select(`
id, horas_totais, horas_concluidas, horas_diarias, nota_final, avaliacao_url, data_inicio, data_fim,
alunos (id, nome, turma_curso, n_escola, ano),
empresas (nome, tutor_nome)
empresas (nome, tutor_nome),
autoavaliacoes (id, assiduidade, responsabilidade, relacionamento, conhecimentos, nota_desejada, comentarios, criado_em)
`)
.order('data_inicio', { ascending: false });
@@ -83,6 +83,7 @@ export default function GestaoRelatorios() {
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;
const autoavaliacao = Array.isArray(estagio.autoavaliacoes) ? estagio.autoavaliacoes[0] : estagio.autoavaliacoes;
const nomeCursoFormatado = formatarTexto(aluno?.turma_curso);
const anoAluno = aluno?.ano || 10;
@@ -104,7 +105,8 @@ export default function GestaoRelatorios() {
horas_totais: estagio.horas_totais || 0,
horas_concluidas: estagio.horas_concluidas || 0,
nota_empresa: estagio.nota_final,
pdf_empresa: estagio.avaliacao_url
pdf_empresa: estagio.avaliacao_url,
autoavaliacao: autoavaliacao || null
};
}) || [];
@@ -141,9 +143,66 @@ export default function GestaoRelatorios() {
}, [relatorios, turmaAtiva]);
// =====================================================================
// GERAÇÃO DO MOTOR HTML DA MATRIZ (USADO NO EXCEL E NO PDF)
// GERAÇÃO DO CABEÇALHO E RODAPÉ COM OS SÍMBOLOS E MARCA DE ÁGUA
// =====================================================================
const construirHtmlMatriz = async (logoEPVC_b64: string, logoEstagios_b64: string) => {
const gerarCabecalhoERodape = async () => {
const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc3.png'));
const b64App = await getBase64Image(require('../../../assets/images/logo_s/texto.png'));
// A imagem que tem os vários logótipos em baixo
const b64LogosFinais = await getBase64Image(require('../../../assets/images/logoepvc.png'));
return {
header: `
<table style="width: 100%; border: none; margin-bottom: 20px;">
<tr>
<td style="width: 25%; text-align: left; vertical-align: top; border: none;">
<img src="${b64Escola}" style="width: 130px; height: auto; display: block;" />
</td>
<td style="width: 50%; text-align: center; border: none;">
</td>
<td style="width: 25%; text-align: right; vertical-align: top; border: none;">
<img src="${b64App}" style="width: 90px; height: auto; display: block; margin-left: auto;" />
</td>
</tr>
</table>
`,
footer: `
<div style="position: absolute; bottom: 10mm; left: 0; right: 0; text-align: center;">
<img src="${b64LogosFinais}" style="height: 40px; width: auto; display: inline-block;" />
<div style="font-size: 9px; color: #64748B; margin-top: 10px; font-style: italic;">
Documento gerado digitalmente via plataforma Estágios+ EPVC &bull; ${new Date().toLocaleDateString('pt-PT')}
</div>
</div>
`,
watermark: `
<div style="position: fixed; top: 35%; left: 15%; opacity: 0.04; z-index: -100; transform: rotate(-30deg); pointer-events: none;">
<img src="${b64Escola}" style="width: 550px;" />
</div>
`
};
};
const getEstilosHTML = () => `
<style>
.main-table { border-collapse: collapse; width: 100%; }
.th-blue { background-color: #003049; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-size: 11px; padding: 4px; }
.th-orange { background-color: #f18721; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-size: 11px; padding: 4px; }
.th-total { background-color: #0f172a; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-size: 11px; padding: 4px; }
.td-cell { border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; mso-number-format: "0"; font-size: 10px; padding: 4px; }
.td-total-cell { border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-weight: bold; background-color: #e2e8f0; font-size: 10px; padding: 4px; }
.td-name { border: 1px solid #cbd5e1; text-align: left; vertical-align: middle; font-weight: bold; white-space: nowrap; padding: 4px 6px; font-size: 11px; }
.zebra { background-color: #F8FAFC; }
</style>
`;
// =====================================================================
// 1 & 2. GERAÇÃO DA MATRIZ EM HTML (USADA NO EXCEL E NO PDF HORIZONTAL)
// =====================================================================
const construirHtmlMatriz = async () => {
const { header, footer } = await gerarCabecalhoERodape();
const tituloMatriz = `<h1 style="color: #003049; margin: 0; font-size: 18px; font-family: sans-serif;">Mapa Oficial de Assiduidade</h1><h2 style="color: #F18721; margin: 5px 0 0; font-size: 12px; font-family: sans-serif;">Formação em Contexto de Trabalho</h2>`;
const headerCompleto = header.replace('', tituloMatriz);
const alunosIds = relatoriosFiltrados.map(r => r.aluno_id);
const { data: presencas } = await supabase
.from('presencas')
@@ -171,7 +230,7 @@ export default function GestaoRelatorios() {
const dia = current.getDate();
const dw = current.getDay();
if (dw !== 0 && dw !== 6) { // Ignorar Sábados e Domingos
if (dw !== 0 && dw !== 6) {
const dataStr = `${ano}-${String(mes+1).padStart(2,'0')}-${String(dia).padStart(2,'0')}`;
let mesObj = meses.find(m => m.nome === `${nomeMes} ${ano}`);
if (!mesObj) { mesObj = { nome: `${nomeMes} ${ano}`, dias: [] }; meses.push(mesObj); }
@@ -180,9 +239,6 @@ export default function GestaoRelatorios() {
current.setDate(current.getDate() + 1);
}
let totalColsDias = meses.reduce((acc: number, m: any) => acc + m.dias.length + 1, 0);
if (totalColsDias === 0) totalColsDias = 1;
let mesesHtml = `<tr style="height: 35px;">
<td class="th-blue" style="width: 40px;">Nº</td>
<td class="th-blue" style="width: 250px;">Nome do Aluno</td>`;
@@ -197,9 +253,9 @@ export default function GestaoRelatorios() {
diasHtml += `<td class="th-total" style="width: 40px;">Total</td>`;
});
mesesHtml += `<td class="th-orange" style="width: 120px;">Horas Lecionadas (1)</td>
<td class="th-orange" style="width: 120px;">Horas por Lecionar (2)</td>
<td class="th-orange" style="width: 120px;">Total Contrato (3)</td></tr>`;
mesesHtml += `<td class="th-orange" style="width: 120px;">Horas Lecionadas</td>
<td class="th-orange" style="width: 120px;">Horas por Lecionar</td>
<td class="th-orange" style="width: 120px;">Total Contrato</td></tr>`;
diasHtml += `<td class="th-blue"></td><td class="th-blue"></td><td class="th-blue"></td></tr>`;
let alunosHtml = '';
@@ -246,73 +302,33 @@ export default function GestaoRelatorios() {
});
return `
<table class="main-table">
<tr>
<td colspan="4" rowspan="2" style="text-align: center; vertical-align: middle; padding: 10px;">
<img src="${logoEPVC_b64}" width="140" style="object-fit: contain; background: transparent; mix-blend-mode: multiply;" />
</td>
<td colspan="${totalColsDias - 3}" rowspan="2" class="title-escola" style="text-align: center; vertical-align: middle;">
Escola Profissional de Vila do Conde<br/>
<span style="font-size: 16pt; color: #f18721;">Mapa Oficial de Assiduidade (FCT)</span>
</td>
<td colspan="4" rowspan="2" style="text-align: center; vertical-align: middle; padding: 10px;">
<img src="${logoEstagios_b64}" width="140" style="object-fit: contain; background: transparent; mix-blend-mode: multiply;" />
</td>
</tr>
<tr></tr>
<tr><td colspan="${totalColsDias + 5}"></td></tr>
<tr>
<td colspan="4" class="subtitle">Período: ${new Date(minDate).toLocaleDateString('pt-PT')} a ${new Date(maxDate).toLocaleDateString('pt-PT')}</td>
<td colspan="${totalColsDias - 3}" class="subtitle" style="text-align: center;">Curso / Turma: ${turmaAtiva || 'Todas as Turmas'}</td>
<td colspan="4" class="subtitle" style="text-align: right;">Ano Letivo: 2025/2026</td>
</tr>
<tr><td colspan="${totalColsDias + 5}"></td></tr>
${mesesHtml}
${diasHtml}
${alunosHtml}
<tr><td colspan="${totalColsDias + 5}"></td></tr>
<tr>
<td colspan="6" style="font-style: italic; color: #64748b; font-size: 11px;">(1) - Horas Lecionadas no período</td>
<td colspan="${Math.max(1, totalColsDias - 7)}" style="font-style: italic; color: #64748b; font-size: 11px;">(2) - Horas em falta para lecionar</td>
<td colspan="6" style="font-style: italic; color: #64748b; font-size: 11px;">(3) - Total de Horas do Contrato de Estágio</td>
</tr>
<tr>
<td colspan="15" style="font-style: italic; color: #ef4444; font-weight: bold; font-size: 11px; padding-top: 5px;">
Legenda de Faltas: [ F ] - Falta Justificada | [ FI ] - Falta Injustificada
</td>
</tr>
</table>
<div style="font-family: 'Segoe UI', Arial, sans-serif;">
${headerCompleto}
<table style="width: 100%; margin-bottom: 10px; font-size: 11px;">
<tr>
<td style="color:#64748b; font-weight:bold;">Período: ${new Date(minDate).toLocaleDateString('pt-PT')} a ${new Date(maxDate).toLocaleDateString('pt-PT')}</td>
<td style="text-align: center; color:#64748b; font-weight:bold;">Turma: ${turmaAtiva || 'Geral'}</td>
<td style="text-align: right; color:#64748b; font-weight:bold;">Ano Letivo: 2025/2026</td>
</tr>
</table>
<table class="main-table">
${mesesHtml}
${diasHtml}
${alunosHtml}
</table>
<div style="margin-top: 15px; font-size: 10px; color: #64748B;">
<strong>Legenda:</strong> <span style="color: #D97706; font-weight: bold;">[ F ]</span> Falta Justificada &bull; <span style="color: #EF4444; font-weight: bold;">[ FI ]</span> Falta Injustificada
</div>
${footer}
</div>
`;
};
const getEstilosHTML = () => `
<style>
.main-table { border-collapse: collapse; font-family: 'Segoe UI', Arial, sans-serif; width: 100%; }
.th-blue { background-color: #003049; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-size: 11px; padding: 4px; }
.th-orange { background-color: #f18721; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-size: 11px; padding: 4px; }
.th-total { background-color: #0f172a; color: #ffffff; font-weight: bold; border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-size: 11px; padding: 4px; }
.td-cell { border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; mso-number-format: "0"; font-size: 10px; padding: 4px; }
.td-total-cell { border: 1px solid #cbd5e1; text-align: center; vertical-align: middle; font-weight: bold; background-color: #e2e8f0; font-size: 10px; padding: 4px; }
.td-name { border: 1px solid #cbd5e1; text-align: left; vertical-align: middle; font-weight: bold; white-space: nowrap; padding: 4px 6px; font-size: 11px; }
.title-escola { font-size: 20pt; font-weight: bold; color: #003049; }
.subtitle { font-size: 12pt; color: #64748b; font-weight: bold; padding: 5px 0; }
.zebra { background-color: #F8FAFC; }
</style>
`;
// 1. EXPORTAR A MATRIZ EM FORMATO EXCEL NATIVO (.XLS)
const gerarExcelGeral = async () => {
if (relatoriosFiltrados.length === 0) return Alert.alert("Aviso", "Não há alunos para exportar nesta turma.");
setGerandoDocumento("excel");
try {
const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc3.png'));
const b64App = await getBase64Image(require('../../../assets/images/logo_s/texto.png'));
const htmlCorpo = await construirHtmlMatriz(b64Escola, b64App);
const htmlCorpo = await construirHtmlMatriz();
const docExcel = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/html40">
<head>
@@ -345,15 +361,11 @@ export default function GestaoRelatorios() {
}
};
// 2. EXPORTAR A MESMA MATRIZ EM FORMATO PDF
const gerarPautaPDF = async () => {
if (relatoriosFiltrados.length === 0) return Alert.alert("Aviso", "Não há alunos para exportar nesta turma.");
setGerandoDocumento("pdf");
try {
const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc3.png'));
const b64App = await getBase64Image(require('../../../assets/images/logo_s/texto.png'));
const htmlCorpo = await construirHtmlMatriz(b64Escola, b64App);
const htmlCorpo = await construirHtmlMatriz();
const htmlCompletoPDF = `
<!DOCTYPE html>
@@ -389,6 +401,95 @@ export default function GestaoRelatorios() {
}
};
// =====================================================================
// 3. EXPORTAR A AUTOAVALIAÇÃO DO ALUNO
// =====================================================================
const gerarAutoavaliacaoPDF = async (r: any) => {
if (!r.autoavaliacao) return Alert.alert("Aviso", "Este aluno ainda não submeteu a sua autoavaliação.");
setGerandoDocumento(`auto_${r.id_estagio}`);
try {
const { header, footer, watermark } = await gerarCabecalhoERodape();
const tituloAuto = `<h1 style="color: #003049; margin: 0; font-size: 18px; font-family: sans-serif;">Ficha de Autoavaliação</h1><h2 style="color: #F18721; margin: 5px 0 0; font-size: 12px; font-family: sans-serif;">Formação em Contexto de Trabalho</h2>`;
const headerCompleto = header.replace('', tituloAuto);
const av = r.autoavaliacao;
const htmlContent = `
<!DOCTYPE html>
<html lang="pt-PT">
<head>
<style>
@page { size: A4; margin: 0; } html, body { height: 99%; overflow: hidden; }
body { font-family: sans-serif; margin: 0; padding: 15mm 20mm; color: #1E293B; font-size: 12px; }
.info-table { width: 100%; border-collapse: collapse; margin-bottom: 25px; margin-top: 10px; font-size: 12px; border: 1px solid #CBD5E1; }
.info-table td { padding: 6px 10px; border: 1px solid #CBD5E1; } .info-table td.label { background-color: #F8FAFC; font-weight: bold; width: 25%; }
.section-title { background-color: #003049; color: white; padding: 6px 10px; font-size: 12px; font-weight: bold; margin-bottom: 10px; }
.eval-table { width: 100%; border-collapse: collapse; margin-bottom: 25px; font-size: 12px; }
.eval-table th { background-color: #F1F5F9; padding: 8px; border: 1px solid #CBD5E1; text-align: left; }
.eval-table td { border: 1px solid #CBD5E1; padding: 8px 10px; }
.eval-table .score { text-align: center; font-weight: bold; font-size: 14px; width: 25%; }
.comments-box { border: 1px solid #CBD5E1; padding: 15px; min-height: 80px; background-color: #F8FAFC; font-style: italic; font-size: 12px; line-height: 1.6; }
</style>
</head>
<body>
${watermark}
${headerCompleto}
<table class="info-table">
<tr><td class="label">Estagiário(a):</td><td><strong>${r.aluno_nome}</strong></td></tr>
<tr><td class="label">Turma e Curso:</td><td>${r.turma}</td></tr>
<tr><td class="label">Entidade de Acolhimento:</td><td><strong>${r.empresa_nome}</strong></td></tr>
<tr><td class="label">Data de Submissão:</td><td>${new Date(av.criado_em).toLocaleDateString('pt-PT')}</td></tr>
</table>
<div style="display: flex; flex-direction: row; gap: 20px; align-items: flex-start;">
<div style="flex: 2;">
<div class="section-title">Critérios Qualitativos</div>
<table class="eval-table">
<tr><th>Domínio Avaliado</th><th style="text-align: center;">Escala (1 a 5)</th></tr>
<tr><td>Assiduidade e Pontualidade</td><td class="score">${av.assiduidade}</td></tr>
<tr><td>Responsabilidade e Iniciativa</td><td class="score">${av.responsabilidade}</td></tr>
<tr><td>Relacionamento Interpessoal</td><td class="score">${av.relacionamento}</td></tr>
<tr><td>Aplicação de Conhecimentos Práticos</td><td class="score">${av.conhecimentos}</td></tr>
</table>
</div>
<div style="flex: 1;">
<div class="section-title">Nota Final</div>
<div style="text-align: center; border: 2px solid #E2E8F0; padding: 25px 0; background-color: #F8FAFC;">
<div style="font-size: 45px; font-weight: 900; color: #003049;">${av.nota_desejada}</div>
<div style="font-size: 14px; color: #64748B; font-weight: bold; margin-top: 5px;">Valores</div>
</div>
</div>
</div>
<div class="section-title">Justificação e Comentários do Aluno</div>
<div class="comments-box">
${av.comentarios ? av.comentarios.replace(/\n/g, '<br/>') : '<em>O aluno não adicionou comentários ou justificação à sua nota.</em>'}
</div>
${footer}
</body>
</html>
`;
const { uri } = await Print.printToFileAsync({ html: htmlContent });
const safeName = r.aluno_nome.replace(/[^a-zA-Z0-9]/g, '_');
const newFileUri = `${FileSystem.documentDirectory}Autoavaliacao_${safeName}.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', 'Não foi possível gerar a autoavaliação.');
} finally {
setGerandoDocumento(null);
}
};
// =====================================================================
// 4. EXPORTAR O DIÁRIO DE BORDO INDIVIDUAL
// =====================================================================
const gerarSumariosPDF = async (r: any) => {
setGerandoDocumento(r.id_estagio);
try {
@@ -413,8 +514,9 @@ export default function GestaoRelatorios() {
linhasTabela += `<tr><td style="text-align: center;">${new Date(s.data).toLocaleDateString('pt-PT')}</td><td style="text-align: center;"><strong>${s.tipo}</strong></td><td>${s.sumario || '<em>Sem descrição.</em>'}</td></tr>`;
});
const b64Escola = await getBase64Image(require('../../../assets/images/logoepvc3.png'));
const b64App = await getBase64Image(require('../../../assets/images/logo_s/texto.png'));
const { header, footer, watermark } = await gerarCabecalhoERodape();
const tituloDiario = `<h1 style="color: #003049; margin: 0; font-size: 18px; font-family: sans-serif;">Diário de Bordo</h1><h2 style="color: #F18721; margin: 5px 0 0; font-size: 12px; font-family: sans-serif;">Formação em Contexto de Trabalho</h2>`;
const headerCompleto = header.replace('', tituloDiario);
const htmlContent = `
<!DOCTYPE html>
@@ -423,30 +525,24 @@ export default function GestaoRelatorios() {
<style>
@page { size: A4; margin: 0; } html, body { height: 99%; overflow: hidden; }
body { font-family: sans-serif; margin: 0; padding: 12mm 15mm; color: #1E293B; font-size: 10px; }
.header-table { width: 100%; border-bottom: 2px solid #003049; padding-bottom: 5px; margin-bottom: 8px; }
.header-center { text-align: center; } .header-center h1 { color: #003049; margin: 0; font-size: 15px; } .header-center h2 { color: #F18721; margin: 2px 0 0; font-size: 9px; }
.info-table { width: 100%; border-collapse: collapse; margin-bottom: 10px; font-size: 9px; border: 1px solid #CBD5E1; }
.info-table td { padding: 4px 6px; border: 1px solid #CBD5E1; } .info-table td.label { background-color: #F8FAFC; font-weight: bold; width: 15%; }
.section-title { background-color: #003049; color: white; padding: 4px 8px; font-size: 9px; font-weight: bold; margin-bottom: 5px; }
.eval-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 9px; } .eval-table th { background-color: #F1F5F9; padding: 4px; border: 1px solid #CBD5E1; } .eval-table td { border: 1px solid #CBD5E1; padding: 3px 8px; }
.footer { text-align: center; padding-top: 5px; border-top: 1px solid #E2E8F0; position: absolute; bottom: 10mm; left: 0; right: 0; margin: 0 auto; width: calc(100% - 30mm); }
</style>
</head>
<body>
<table class="header-table">
<tr>
<td style="width: 25%;"><img src="${b64Escola}" width="110" style="mix-blend-mode: multiply;" /></td>
<td style="width: 50%;" class="header-center"><h1>Diário de Bordo</h1><h2>Formação em Contexto de Trabalho</h2></td>
<td style="width: 25%; text-align: right;"><img src="${b64App}" width="110" style="mix-blend-mode: multiply;" /></td>
</tr>
</table>
${watermark}
${headerCompleto}
<table class="info-table">
<tr><td class="label">Estagiário:</td><td><strong>${r.aluno_nome}</strong></td><td class="label">Turma:</td><td>${r.turma}</td></tr>
<tr><td class="label">Entidade:</td><td><strong>${r.empresa_nome}</strong></td><td class="label">Tutor(a):</td><td>${r.tutor_nome}</td></tr>
</table>
<div class="section-title">Registo de Atividades Diárias</div>
<table class="eval-table"><tr><th style="width: 15%;">Data</th><th style="width: 20%;">Natureza</th><th style="width: 65%;">Sumário</th></tr>${linhasTabela}</table>
<div class="footer"><div class="footer-text">Documento gerado digitalmente via plataforma Estágios+ EPVC. Data: ${new Date().toLocaleDateString('pt-PT')}</div></div>
${footer}
</body>
</html>
`;
@@ -597,6 +693,7 @@ export default function GestaoRelatorios() {
<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>
@@ -607,36 +704,61 @@ export default function GestaoRelatorios() {
)}
</View>
{r.pdf_empresa && (
<TouchableOpacity style={styles.btnAcaoLigeiro} onPress={() => WebBrowser.openBrowserAsync(r.pdf_empresa)}>
<Ionicons name="document-text" size={16} color={cores.azulMarinho} />
<TouchableOpacity
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
onPress={() => WebBrowser.openBrowserAsync(r.pdf_empresa)}
>
<Ionicons name="reader-outline" size={20} 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>
{r.autoavaliacao ? (
<Text style={[styles.notaTag, { backgroundColor: cores.verdeAgua + '30', color: '#003049' }]}>Concluída</Text>
) : (
<Text style={[styles.notaTag, { backgroundColor: cores.laranja + '20', color: cores.laranja }]}>Pendente</Text>
)}
</View>
{r.autoavaliacao && (
<TouchableOpacity
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
onPress={() => gerarAutoavaliacaoPDF(r)}
disabled={gerandoDocumento === `auto_${r.id_estagio}`}
>
{gerandoDocumento === `auto_${r.id_estagio}` ? (
<ActivityIndicator size="small" color={cores.azulMarinho} />
) : (
<>
<Ionicons name="person-circle-outline" size={20} color={cores.azulMarinho} />
<Text style={[styles.textoAcao, { color: cores.azulMarinho }]}>Exportar Autoavaliação (PDF)</Text>
</>
)}
</TouchableOpacity>
)}
</View>
{/* 3. DIÁRIO DE BORDO */}
<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 }]}
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
onPress={() => gerarSumariosPDF(r)}
disabled={gerandoDocumento === r.id_estagio}
>
{gerandoDocumento === r.id_estagio ? (
<ActivityIndicator size="small" color={cores.laranja} />
<ActivityIndicator size="small" color={cores.azulMarinho} />
) : (
<>
<Ionicons name="calendar-outline" size={16} color={cores.laranja} />
<Text style={[styles.textoAcao, { color: cores.laranja }]}>Exportar Diário (PDF)</Text>
<Ionicons name="journal-outline" size={20} color={cores.azulMarinho} />
<Text style={[styles.textoAcao, { color: cores.azulMarinho }]}>Exportar Diário (PDF)</Text>
</>
)}
</TouchableOpacity>
@@ -688,6 +810,16 @@ const styles = StyleSheet.create({
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' }
btnAcaoLigeiro: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
paddingVertical: 12,
borderRadius: 12,
borderWidth: 1,
marginTop: 12,
gap: 8
},
textoAcao: { fontSize: 13, fontWeight: '800' }
});

1801
package-lock.json generated

File diff suppressed because it is too large Load Diff