updates
This commit is contained in:
@@ -4,13 +4,13 @@ import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
@@ -48,36 +48,43 @@ export default function AvaliacoesEmpresaLista() {
|
||||
|
||||
if (!empresa) return;
|
||||
|
||||
// Vai buscar os estagios desta empresa e cruza com os nomes dos alunos
|
||||
// CORREÇÃO: Vamos buscar a nota diretamente da tabela 'avaliacoes_empresa' através do relacionamento
|
||||
const { data, error } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
id,
|
||||
nota_final,
|
||||
alunos (id, nome)
|
||||
alunos (id, nome),
|
||||
avaliacoes_empresa (nota_final)
|
||||
`)
|
||||
.eq('empresa_id', empresa.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (data) {
|
||||
// Formatar os dados para a lista, lidando com o picuinhas do TypeScript
|
||||
const listaFormatada = data.map((estagio: any) => {
|
||||
// Extrair o aluno em segurança, quer venha como objeto ou como lista (Array)
|
||||
const aluno = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos;
|
||||
|
||||
// Extrai a avaliação em segurança (visto que pode vir como objeto único ou array dependendo da relação da BD)
|
||||
const avaliacao = Array.isArray(estagio.avaliacoes_empresa)
|
||||
? estagio.avaliacoes_empresa[0]
|
||||
: estagio.avaliacoes_empresa;
|
||||
|
||||
// Obtém a nota real guardada no Supabase
|
||||
const notaReal = avaliacao?.nota_final;
|
||||
const foiAvaliado = notaReal !== null && notaReal !== undefined;
|
||||
|
||||
return {
|
||||
estagio_id: estagio.id,
|
||||
aluno_id: aluno?.id,
|
||||
aluno_nome: aluno?.nome || 'Aluno Desconhecido',
|
||||
nota: estagio.nota_final,
|
||||
avaliado: estagio.nota_final !== null && estagio.nota_final !== undefined
|
||||
nota: foiAvaliado ? Number(notaReal) : null,
|
||||
avaliado: foiAvaliado
|
||||
};
|
||||
});
|
||||
setEstagios(listaFormatada);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Erro ao listar avaliações:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -91,7 +98,13 @@ export default function AvaliacoesEmpresaLista() {
|
||||
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push({
|
||||
pathname: '/Empresas/fichaAvaliacao',
|
||||
params: { estagio_id: item.estagio_id, aluno_nome: item.aluno_nome, nota_atual: item.nota }
|
||||
// Passamos também os dados adicionais necessários para preencher a ficha corretamente
|
||||
params: {
|
||||
estagio_id: item.estagio_id,
|
||||
aluno_nome: item.aluno_nome,
|
||||
aluno_turma: item.aluno_turma || '',
|
||||
n_escola: item.aluno_id || ''
|
||||
}
|
||||
})}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
@@ -99,14 +112,17 @@ export default function AvaliacoesEmpresaLista() {
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azul + '15' }]}>
|
||||
<Ionicons name="person" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.alunoName, { color: cores.texto }]}>{item.aluno_nome}</Text>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.alunoName, { color: cores.texto }]} numberOfLines={1}>
|
||||
{item.aluno_nome}
|
||||
</Text>
|
||||
<Text style={[styles.statusText, { color: item.avaliado ? cores.verde : cores.laranja }]}>
|
||||
{item.avaliado ? '✓ Avaliação Concluída' : 'Aguardando Avaliação'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* CORREÇÃO VISUAL: Mostra apenas a nota real do Supabase sem strings estáticas duplicadas */}
|
||||
{item.avaliado ? (
|
||||
<View style={[styles.gradeBadge, { backgroundColor: cores.verde + '15' }]}>
|
||||
<Text style={[styles.gradeText, { color: cores.verde }]}>{item.nota} / 20</Text>
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
// Corrigido para usar a biblioteca recomendada e evitar o Warning
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
@@ -46,21 +46,35 @@ export default function DetalhesAlunoEmpresa() {
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Buscar dados do Estágio e do Aluno (Removido o campo cargo)
|
||||
// 1. Buscar dados do Estágio, Aluno e adicionada a relação com a tabela avaliacoes_empresa
|
||||
const { data, error } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
id, data_inicio, data_fim, horas_totais, horas_concluidas,
|
||||
nota_final, avaliacao_url,
|
||||
alunos (id, nome, turma_curso, n_escola, profile_id, ano)
|
||||
estado,
|
||||
alunos (id, nome, turma_curso, n_escola, profile_id, ano),
|
||||
avaliacoes_empresa (nota_final)
|
||||
`)
|
||||
.eq('id', estagio_id)
|
||||
.single();
|
||||
.maybeSingle(); // Alterado para maybeSingle para evitar quebras caso não encontre
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (!data) {
|
||||
Alert.alert('Aviso', 'Estágio não encontrado.');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
const alunoData = Array.isArray(data.alunos) ? data.alunos[0] : data.alunos;
|
||||
|
||||
// Extrair nota final da tabela avaliacoes_empresa (pode vir como objeto único ou array)
|
||||
const avaliacaoData = Array.isArray(data.avaliacoes_empresa)
|
||||
? data.avaliacoes_empresa[0]
|
||||
: data.avaliacoes_empresa;
|
||||
|
||||
const notaReal = avaliacaoData?.nota_final ?? null;
|
||||
|
||||
// 2. Buscar detalhes no Profile (Schema: residencia, data_nascimento, telefone, email)
|
||||
let infoExtra = { telefone: 'N/A', email: 'N/A', residencia: 'N/A', d_nasc: 'N/A' };
|
||||
|
||||
@@ -81,7 +95,8 @@ export default function DetalhesAlunoEmpresa() {
|
||||
}
|
||||
}
|
||||
|
||||
setEstagio({ ...data, aluno: alunoData, infoExtra });
|
||||
// Adicionamos a nota_real ao nosso estado local do estágio
|
||||
setEstagio({ ...data, aluno: alunoData, infoExtra, nota_real: notaReal });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -110,8 +125,11 @@ export default function DetalhesAlunoEmpresa() {
|
||||
|
||||
const progresso = estagio?.horas_totais > 0 ? (estagio.horas_concluidas / estagio.horas_totais) * 100 : 0;
|
||||
|
||||
// Verifica se a nota real existe na tabela avaliacoes_empresa
|
||||
const temNota = estagio?.nota_real !== undefined && estagio?.nota_real !== null;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.header}>
|
||||
@@ -122,6 +140,7 @@ export default function DetalhesAlunoEmpresa() {
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchDetalhes(true); }} />}
|
||||
>
|
||||
{/* CABEÇALHO DE IDENTIFICAÇÃO */}
|
||||
@@ -191,25 +210,47 @@ export default function DetalhesAlunoEmpresa() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* AVALIAÇÃO */}
|
||||
{/* AVALIAÇÃO CORRIGIDA AUTOMÁTICA */}
|
||||
<Text style={styles.sectionTitle}>Documentação Oficial</Text>
|
||||
{estagio.nota_final ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.cardAvaliado, { backgroundColor: cores.azulMarinho }]}
|
||||
onPress={() => WebBrowser.openBrowserAsync(estagio.avaliacao_url)}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.textoBranco}>Estagiário Avaliado</Text>
|
||||
<Text style={styles.notaTexto}>Classificação: {estagio.nota_final} Valores</Text>
|
||||
{temNota ? (
|
||||
<View style={{ gap: 10 }}>
|
||||
<View style={[styles.cardAvaliado, { backgroundColor: cores.azulMarinho }]}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.textoBranco}>Estagiário Avaliado</Text>
|
||||
{/* Agora exibe dinamicamente a nota inserida pela empresa */}
|
||||
<Text style={styles.notaTexto}>Classificação: {estagio.nota_real} Valores</Text>
|
||||
</View>
|
||||
<Ionicons name="checkmark-circle" size={30} color={cores.verdeAgua} />
|
||||
</View>
|
||||
<Ionicons name="document-text" size={30} color="#FFF" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Mantemos o botão de atualizar/editar a avaliação caso queiram alterar alguma coisa */}
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAvaliar, { backgroundColor: cores.azulMarinho, borderWidth: 1.5, borderColor: cores.borda }]}
|
||||
onPress={() => router.push({
|
||||
pathname: '/Empresas/fichaAvaliacao',
|
||||
params: {
|
||||
estagio_id: estagio.id,
|
||||
aluno_nome: estagio.aluno?.nome,
|
||||
aluno_turma: estagio.aluno?.turma_curso,
|
||||
n_escola: estagio.aluno?.n_escola
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Ionicons name="create-outline" size={20} color="#FFF" />
|
||||
<Text style={styles.btnTexto}>Editar / Consultar Avaliação</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAvaliar, { backgroundColor: cores.laranja }]}
|
||||
onPress={() => router.push({
|
||||
pathname: '/Empresas/fichaAvaliacao',
|
||||
params: { estagio_id: estagio.id, aluno_nome: estagio.aluno?.nome }
|
||||
params: {
|
||||
estagio_id: estagio.id,
|
||||
aluno_nome: estagio.aluno?.nome,
|
||||
aluno_turma: estagio.aluno?.turma_curso,
|
||||
n_escola: estagio.aluno?.n_escola
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Ionicons name="create-outline" size={22} color="#FFF" />
|
||||
@@ -244,9 +285,9 @@ const styles = StyleSheet.create({
|
||||
progressoHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 },
|
||||
barBg: { height: 10, backgroundColor: '#E2E8F0', borderRadius: 5, overflow: 'hidden' },
|
||||
barFill: { height: '100%', borderRadius: 5 },
|
||||
cardAvaliado: { padding: 20, borderRadius: 20, flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
cardAvaliado: { padding: 20, borderRadius: 20, flexDirection: 'row', alignItems: 'center' },
|
||||
textoBranco: { color: '#FFF', fontSize: 14, fontWeight: '600' },
|
||||
notaTexto: { color: '#FFF', fontSize: 18, fontWeight: '900' },
|
||||
notaTexto: { color: '#FFF', fontSize: 18, fontWeight: '900', marginTop: 2 },
|
||||
btnAvaliar: { padding: 18, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 10 },
|
||||
btnTexto: { color: '#FFF', fontSize: 16, fontWeight: '900' }
|
||||
});
|
||||
@@ -1,15 +1,18 @@
|
||||
// app/Empresa/FichaAvaliacao.tsx
|
||||
// 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';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Print from 'expo-print';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
@@ -18,6 +21,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
@@ -43,7 +47,22 @@ const ESCALA = [
|
||||
{ valor: 5, label: 'Excelente' },
|
||||
];
|
||||
|
||||
// ─── Componente ───────────────────────────────────────────────────────────────
|
||||
// ─── 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();
|
||||
@@ -54,10 +73,17 @@ 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 || '—');
|
||||
|
||||
const [respostas, setRespostas] = useState<Record<string, number>>({});
|
||||
const [notaFinal, setNotaFinal] = useState('');
|
||||
const [observacoes, setObservacoes] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [gerandoPDF, setGerandoPDF] = useState(false);
|
||||
const [loadingDados, setLoadingDados] = useState(true);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
@@ -73,7 +99,69 @@ export default function FichaAvaliacao() {
|
||||
inputFundo: isDarkMode ? '#1E1E20' : '#F8FAFC',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Cor do botão de escala consoante o valor selecionado
|
||||
// ─── Carregar dados existentes ao entrar na página com Correção de Fallback ───
|
||||
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')
|
||||
.eq('estagio_id', params.estagio_id)
|
||||
.maybeSingle();
|
||||
|
||||
if (errAvaliacao) throw errAvaliacao;
|
||||
|
||||
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());
|
||||
}
|
||||
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
|
||||
)
|
||||
`)
|
||||
.eq('id', params.estagio_id)
|
||||
.maybeSingle();
|
||||
|
||||
if (!errEstagio && estagio?.alunos) {
|
||||
const al = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos;
|
||||
if (al) {
|
||||
if (al.nome) setAlunoNome(al.nome);
|
||||
if (al.turma_curso) setAlunoTurma(al.turma_curso);
|
||||
if (al.n_escola) setNumEscola(al.n_escola.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erro ao carregar dados da avaliação:', 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 }> = {
|
||||
@@ -86,8 +174,8 @@ export default function FichaAvaliacao() {
|
||||
return mapa[valor];
|
||||
};
|
||||
|
||||
// Validação da nota final
|
||||
const notaValida = useMemo(() => {
|
||||
if (!notaFinal.trim()) return false;
|
||||
const n = parseFloat(notaFinal.replace(',', '.'));
|
||||
return !isNaN(n) && n >= 0 && n <= 20;
|
||||
}, [notaFinal]);
|
||||
@@ -95,7 +183,6 @@ export default function FichaAvaliacao() {
|
||||
const todasRespondidas = PERGUNTAS.every(p => respostas[p.id] !== undefined);
|
||||
const podeSometer = todasRespondidas && notaValida;
|
||||
|
||||
// Agrupa perguntas por categoria
|
||||
const categorias = useMemo(() => {
|
||||
const mapa: Record<string, typeof PERGUNTAS> = {};
|
||||
PERGUNTAS.forEach(p => {
|
||||
@@ -105,6 +192,7 @@ export default function FichaAvaliacao() {
|
||||
return mapa;
|
||||
}, []);
|
||||
|
||||
// ─── Submeter Avaliação ──────────────────────────────────────────────────────
|
||||
const handleSubmit = async () => {
|
||||
if (!podeSometer) {
|
||||
Alert.alert('Atenção', 'Preenche todas as questões e insere uma nota final válida (0–20).');
|
||||
@@ -112,8 +200,8 @@ export default function FichaAvaliacao() {
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Submeter Avaliação',
|
||||
`Confirmas a avaliação de ${params.aluno_nome} com nota final ${notaFinal} valores?`,
|
||||
'Salvar Avaliação',
|
||||
`Confirmas a avaliação de ${alunoNome} com nota final de ${notaFinal} valores?`,
|
||||
[
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{
|
||||
@@ -123,27 +211,34 @@ export default function FichaAvaliacao() {
|
||||
try {
|
||||
const notaNum = parseFloat(notaFinal.replace(',', '.'));
|
||||
|
||||
// Guarda as respostas individuais + nota + observações
|
||||
const { error } = await supabase
|
||||
.from('estagios')
|
||||
.update({
|
||||
.from('avaliacoes_empresa')
|
||||
.upsert({
|
||||
estagio_id: params.estagio_id,
|
||||
aluno_nome: alunoNome,
|
||||
aluno_n_escola: numEscola && numEscola !== '—' ? parseInt(numEscola, 10) : null,
|
||||
aluno_turma: alunoTurma,
|
||||
nota_final: notaNum,
|
||||
avaliacao_observacoes: observacoes.trim() || null,
|
||||
avaliacao_respostas: respostas, // jsonb no Supabase
|
||||
avaliacao_data: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', params.estagio_id);
|
||||
respostas: respostas,
|
||||
observacoes: observacoes.trim() || null,
|
||||
atualizado_em: new Date().toISOString(),
|
||||
}, { onConflict: 'estagio_id' });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
await supabase
|
||||
.from('estagios')
|
||||
.update({ estado: 'Avaliado' })
|
||||
.eq('id', params.estagio_id);
|
||||
|
||||
Alert.alert(
|
||||
'Avaliação Submetida!',
|
||||
'A ficha foi gravada com sucesso. O professor será notificado.',
|
||||
'Avaliação Guardada!',
|
||||
'A ficha foi gravada com sucesso. O professor poderá agora consultá-la.',
|
||||
[{ text: 'OK', onPress: () => router.back() }]
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Alert.alert('Erro', 'Não foi possível submeter a avaliação. Tenta novamente.');
|
||||
Alert.alert('Erro', 'Não foi possível guardar a avaliação. Tenta novamente.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -153,12 +248,149 @@ export default function FichaAvaliacao() {
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Render ────────────────────────────────────────────────────────────────
|
||||
// ─── Gerar PDF da Avaliação ──────────────────────────────────────────────────
|
||||
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.');
|
||||
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 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>`;
|
||||
});
|
||||
});
|
||||
|
||||
const notaNum = parseFloat(notaFinal.replace(',', '.'));
|
||||
const positiva = notaNum >= 9.5;
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-PT">
|
||||
<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; }
|
||||
</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>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
<div class="section">Avaliação Qualitativa (1 = Insatisfatório • 5 = Excelente)</div>
|
||||
<table class="tbl">${linhasPerguntas}</table>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="footer">
|
||||
Documento gerado digitalmente via plataforma Estágios+ EPVC • ${new Date().toLocaleDateString('pt-PT')}<br/>
|
||||
<img src="${b64Final}" />
|
||||
</div>
|
||||
</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' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Alert.alert('Erro', 'Não foi possível gerar o PDF da avaliação.');
|
||||
} finally {
|
||||
setGerandoPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingDados) {
|
||||
return (
|
||||
<View style={[s.loadingContainer, { backgroundColor: cores.fundo }]}>
|
||||
<ActivityIndicator size="large" color={cores.azulMarinho} />
|
||||
<Text style={[s.loadingText, { color: cores.textoSecundario }]}>A carregar avaliação existente...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[s.safe, { backgroundColor: cores.fundo }]}>
|
||||
<SafeAreaView style={[s.safe, { backgroundColor: cores.fundo }]} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
{/* Header */}
|
||||
<View style={s.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={s.btnBack}>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
@@ -172,21 +404,19 @@ export default function FichaAvaliacao() {
|
||||
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<ScrollView contentContainerStyle={s.scroll} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* Cartão do Aluno */}
|
||||
<View style={[s.alunoCard, { backgroundColor: cores.azulMarinho }]}>
|
||||
<View style={s.alunoAvatar}>
|
||||
<Text style={s.alunoAvatarLetra}>{params.aluno_nome?.charAt(0) ?? '?'}</Text>
|
||||
<Text style={s.alunoAvatarLetra}>{alunoNome?.charAt(0) ?? '?'}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 14 }}>
|
||||
<Text style={s.alunoNome}>{params.aluno_nome}</Text>
|
||||
<Text style={s.alunoMeta}>Nº {params.n_escola} · {params.aluno_turma}</Text>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
{/* Instruções */}
|
||||
<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 }]}>
|
||||
@@ -194,21 +424,18 @@ export default function FichaAvaliacao() {
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Perguntas agrupadas por categoria */}
|
||||
{Object.entries(categorias).map(([categoria, perguntas], catIdx) => (
|
||||
<View key={catIdx}>
|
||||
{/* Cabeçalho de categoria */}
|
||||
<View style={[s.catHeader, { backgroundColor: cores.azulMarinho }]}>
|
||||
<Text style={s.catHeaderTxt}>{categoria}</Text>
|
||||
</View>
|
||||
|
||||
{perguntas.map((pergunta, idx) => {
|
||||
{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 }]}>
|
||||
{/* Número + Texto */}
|
||||
<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>
|
||||
@@ -216,7 +443,6 @@ export default function FichaAvaliacao() {
|
||||
<Text style={[s.perguntaTxt, { color: cores.texto }]}>{pergunta.texto}</Text>
|
||||
</View>
|
||||
|
||||
{/* Escala */}
|
||||
<View style={s.escalaRow}>
|
||||
{ESCALA.map(e => {
|
||||
const esteSelecionado = selecionado === e.valor;
|
||||
@@ -234,7 +460,6 @@ export default function FichaAvaliacao() {
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Label do selecionado */}
|
||||
{selecionado && (
|
||||
<Text style={[s.escalaLabel, { color: cores.textoSecundario }]}>
|
||||
{ESCALA.find(e => e.valor === selecionado)?.label}
|
||||
@@ -246,11 +471,10 @@ export default function FichaAvaliacao() {
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Nota Final */}
|
||||
<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 }]}>
|
||||
Atribui uma nota de <Text style={{ fontWeight: '800' }}>0 a 20 valores</Text>. Abaixo de 9,5 é negativa.
|
||||
Insira a nota obtida na escala de <Text style={{ fontWeight: '800' }}>0 a 20 valores</Text>.
|
||||
</Text>
|
||||
|
||||
<View style={s.notaRow}>
|
||||
@@ -263,14 +487,14 @@ export default function FichaAvaliacao() {
|
||||
color: cores.texto,
|
||||
},
|
||||
]}
|
||||
placeholder="Ex: 16"
|
||||
placeholder="0-20"
|
||||
placeholderTextColor={cores.textoSecundario}
|
||||
keyboardType="decimal-pad"
|
||||
value={notaFinal}
|
||||
onChangeText={setNotaFinal}
|
||||
maxLength={4}
|
||||
/>
|
||||
<Text style={[s.notaSufixo, { color: cores.textoSecundario }]}>/ 20 valores</Text>
|
||||
<Text style={[s.notaSufixo, { color: cores.textoSecundario }]}>valores</Text>
|
||||
|
||||
{notaFinal !== '' && (
|
||||
<View style={[
|
||||
@@ -290,7 +514,6 @@ export default function FichaAvaliacao() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Observações */}
|
||||
<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>
|
||||
@@ -306,7 +529,6 @@ export default function FichaAvaliacao() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Progresso */}
|
||||
<View style={[s.progressoBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={s.progressoRow}>
|
||||
<Text style={[s.progressoLabel, { color: cores.textoSecundario }]}>
|
||||
@@ -327,27 +549,43 @@ export default function FichaAvaliacao() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Botão Submeter */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
s.btnSubmeter,
|
||||
{ backgroundColor: podeSometer ? cores.azulMarinho : cores.borda },
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
disabled={!podeSometer || submitting}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{submitting ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="checkmark-circle" size={22} color={podeSometer ? '#fff' : cores.textoSecundario} />
|
||||
<Text style={[s.btnSubmeterTxt, { color: podeSometer ? '#fff' : cores.textoSecundario }]}>
|
||||
Submeter Avaliação
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View style={s.botoesRow}>
|
||||
<TouchableOpacity
|
||||
style={[s.btnSalvar, { backgroundColor: podeSometer ? cores.azulMarinho : cores.borda }]}
|
||||
onPress={handleSubmit}
|
||||
disabled={!podeSometer || submitting}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[s.btnPDF, { backgroundColor: podeSometer ? cores.laranja : cores.borda }]}
|
||||
onPress={gerarPDFAvaliacao}
|
||||
disabled={!podeSometer || gerandoPDF}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={{ height: 30 }} />
|
||||
</ScrollView>
|
||||
@@ -356,7 +594,7 @@ export default function FichaAvaliacao() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Estilos ──────────────────────────────────────────────────────────────────
|
||||
// ─── Estilos Customizados ──────────────────────────────────────────────────────
|
||||
const s = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingTop: 15, paddingBottom: 12, gap: 14 },
|
||||
@@ -365,7 +603,9 @@ const s = StyleSheet.create({
|
||||
headerSub: { fontSize: 12, fontWeight: '500', marginTop: 1 },
|
||||
scroll: { paddingHorizontal: 16, paddingBottom: 20 },
|
||||
|
||||
// Aluno card
|
||||
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' },
|
||||
@@ -374,48 +614,42 @@ const s = StyleSheet.create({
|
||||
badgeFCT: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8 },
|
||||
badgeFCTText: { color: '#fff', fontSize: 11, fontWeight: '900', letterSpacing: 1 },
|
||||
|
||||
// Instruções
|
||||
instrucoes: { flexDirection: 'row', alignItems: 'flex-start', borderRadius: 12, borderWidth: 1, padding: 12, marginBottom: 20 },
|
||||
instrucoesTxt: { flex: 1, fontSize: 13, lineHeight: 19 },
|
||||
|
||||
// Categoria
|
||||
catHeader: { borderRadius: 8, paddingVertical: 7, paddingHorizontal: 14, marginBottom: 10, marginTop: 6 },
|
||||
catHeaderTxt: { color: '#fff', fontSize: 11, fontWeight: '800', letterSpacing: 1, textTransform: 'uppercase' },
|
||||
|
||||
// Pergunta
|
||||
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 },
|
||||
|
||||
// Escala
|
||||
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' },
|
||||
|
||||
// Nota final
|
||||
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: 90, textAlign: 'center' },
|
||||
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' },
|
||||
|
||||
// Observações
|
||||
obsInput: { borderWidth: 1.5, borderRadius: 12, padding: 14, fontSize: 14, minHeight: 100, lineHeight: 21 },
|
||||
|
||||
// Progresso
|
||||
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 },
|
||||
|
||||
// Botão submeter
|
||||
btnSubmeter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, paddingVertical: 17, borderRadius: 18, marginTop: 4 },
|
||||
btnSubmeterTxt: { fontSize: 16, fontWeight: '900' },
|
||||
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' },
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
// app/Professor/Alunos/relatorios.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { Asset } from 'expo-asset';
|
||||
@@ -6,7 +5,6 @@ import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Print from 'expo-print';
|
||||
import { useRouter } from 'expo-router';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -24,6 +22,28 @@ import {
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
// ─── Perguntas/Escala (idênticas às usadas pela empresa) ──────────────────────
|
||||
const PERGUNTAS_EMP = [
|
||||
{ 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.' },
|
||||
];
|
||||
|
||||
const ESCALA_EMP = [
|
||||
{ valor: 1, label: 'Insatisfatório' },
|
||||
{ valor: 2, label: 'A Melhorar' },
|
||||
{ valor: 3, label: 'Satisfatório' },
|
||||
{ valor: 4, label: 'Bom' },
|
||||
{ valor: 5, label: 'Excelente' },
|
||||
];
|
||||
|
||||
export default function GestaoRelatorios() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
@@ -56,7 +76,7 @@ export default function GestaoRelatorios() {
|
||||
return `data:image/png;base64,${base64.replace(/(\r\n|\n|\r)/gm, "")}`;
|
||||
} catch (e) {
|
||||
console.error("Erro no Base64:", e);
|
||||
return "";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,7 +84,7 @@ export default function GestaoRelatorios() {
|
||||
if (!texto || texto === 'N/A') return 'N/A';
|
||||
return texto.charAt(0).toUpperCase() + texto.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
|
||||
const fetchRelatorios = async (isManualRefresh = false) => {
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
try {
|
||||
@@ -72,9 +92,10 @@ export default function GestaoRelatorios() {
|
||||
.from('estagios')
|
||||
.select(`
|
||||
id, horas_totais, horas_concluidas, horas_diarias, nota_final, avaliacao_url, data_inicio, data_fim,
|
||||
alunos (id, nome, turma_curso, n_escola, ano),
|
||||
alunos (id, nome, turma_curso, n_escola, ano),
|
||||
empresas (nome, tutor_nome),
|
||||
autoavaliacoes (id, assiduidade, responsabilidade, relacionamento, conhecimentos, nota_desejada, comentarios, criado_em)
|
||||
autoavaliacoes (id, assiduidade, responsabilidade, relacionamento, conhecimentos, nota_desejada, comentarios, criado_em),
|
||||
avaliacoes_empresa (respostas, observacoes, atualizado_em)
|
||||
`)
|
||||
.order('data_inicio', { ascending: false });
|
||||
|
||||
@@ -84,7 +105,8 @@ export default function GestaoRelatorios() {
|
||||
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 avaliacaoEmp = Array.isArray(estagio.avaliacoes_empresa) ? estagio.avaliacoes_empresa[0] : estagio.avaliacoes_empresa;
|
||||
|
||||
const nomeCursoFormatado = formatarTexto(aluno?.turma_curso);
|
||||
const anoAluno = aluno?.ano || 10;
|
||||
const turmaCompleta = `${anoAluno}º ${nomeCursoFormatado}`;
|
||||
@@ -95,7 +117,7 @@ export default function GestaoRelatorios() {
|
||||
aluno_nome: aluno?.nome || 'Desconhecido',
|
||||
curso: nomeCursoFormatado,
|
||||
ano: anoAluno,
|
||||
turma: turmaCompleta,
|
||||
turma: turmaCompleta,
|
||||
n_escola: aluno?.n_escola || '--',
|
||||
empresa_nome: empresa?.nome || 'N/A',
|
||||
tutor_nome: empresa?.tutor_nome || 'N/A',
|
||||
@@ -105,8 +127,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,
|
||||
autoavaliacao: autoavaliacao || null
|
||||
autoavaliacao: autoavaliacao || null,
|
||||
avaliacao_empresa_detalhes: avaliacaoEmp || null
|
||||
};
|
||||
}) || [];
|
||||
|
||||
@@ -124,16 +146,12 @@ export default function GestaoRelatorios() {
|
||||
|
||||
const cursosAgrupados = useMemo(() => {
|
||||
const grupos: Record<string, { count10: number, count11: number, count12: number }> = {};
|
||||
|
||||
relatorios.forEach(r => {
|
||||
if (!grupos[r.curso]) {
|
||||
grupos[r.curso] = { count10: 0, count11: 0, count12: 0 };
|
||||
}
|
||||
if (!grupos[r.curso]) grupos[r.curso] = { count10: 0, count11: 0, count12: 0 };
|
||||
if (r.ano === 10 || r.ano === 1) grupos[r.curso].count10++;
|
||||
else if (r.ano === 11 || r.ano === 2) grupos[r.curso].count11++;
|
||||
else if (r.ano === 12 || r.ano === 3) grupos[r.curso].count12++;
|
||||
});
|
||||
|
||||
return Object.keys(grupos).map(curso => ({ curso, ...grupos[curso] })).sort((a, b) => a.curso.localeCompare(b.curso));
|
||||
}, [relatorios]);
|
||||
|
||||
@@ -142,45 +160,52 @@ export default function GestaoRelatorios() {
|
||||
return relatorios.filter(r => r.turma === turmaAtiva);
|
||||
}, [relatorios, turmaAtiva]);
|
||||
|
||||
// =====================================================================
|
||||
// GERAÇÃO DO CABEÇALHO E RODAPÉ COM OS SÍMBOLOS E MARCA DE ÁGUA
|
||||
// =====================================================================
|
||||
// ─── Cabeçalho/Rodapé/Marca de água partilhados ────────────────────────────
|
||||
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'));
|
||||
const logoEscola = require('../../assets/logoepvc3.png');
|
||||
const logoEstagiosPlus = require('../../assets/logo_estagios_plus.png');
|
||||
const logoRodape = require('../../assets/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 • ${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 header = `
|
||||
<div style="width: 100%; border-bottom: 2px solid #003049; padding-bottom: 10px; 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>
|
||||
<td id="pdf-title-container" style="width: 45%; 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>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const footer = `
|
||||
<div style="position: fixed; bottom: 0; left: 0; right: 0; text-align: center; font-family: sans-serif; font-size: 8px; color: #94A3B8; border-top: 1px solid #E2E8F0; padding-top: 8px;">
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
return { header, footer, watermark };
|
||||
};
|
||||
|
||||
const getEstilosHTML = () => `
|
||||
<style>
|
||||
@@ -195,9 +220,7 @@ export default function GestaoRelatorios() {
|
||||
</style>
|
||||
`;
|
||||
|
||||
// =====================================================================
|
||||
// 1 & 2. GERAÇÃO DA MATRIZ EM HTML (USADA NO EXCEL E NO PDF HORIZONTAL)
|
||||
// =====================================================================
|
||||
// ─── Matriz de Assiduidade (mantida) ───────────────────────────────────────
|
||||
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>`;
|
||||
@@ -215,7 +238,7 @@ export default function GestaoRelatorios() {
|
||||
if(r.data_inicio < minDateStr) minDateStr = r.data_inicio;
|
||||
if(r.data_fim > maxDateStr) maxDateStr = r.data_fim;
|
||||
});
|
||||
|
||||
|
||||
let minDate = new Date(minDateStr);
|
||||
let maxDate = new Date(maxDateStr);
|
||||
if (isNaN(minDate.getTime())) minDate = new Date();
|
||||
@@ -229,8 +252,7 @@ export default function GestaoRelatorios() {
|
||||
const nomeMes = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'][mes];
|
||||
const dia = current.getDate();
|
||||
const dw = current.getDay();
|
||||
|
||||
if (dw !== 0 && dw !== 6) {
|
||||
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); }
|
||||
@@ -242,14 +264,11 @@ export default function GestaoRelatorios() {
|
||||
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>`;
|
||||
let diasHtml = `<tr style="height: 25px;">
|
||||
<td class="th-blue"></td><td class="th-blue"></td>`;
|
||||
let diasHtml = `<tr style="height: 25px;"><td class="th-blue"></td><td class="th-blue"></td>`;
|
||||
|
||||
meses.forEach((m: any) => {
|
||||
mesesHtml += `<td colspan="${m.dias.length + 1}" class="th-orange">${m.nome}</td>`;
|
||||
m.dias.forEach((d: any) => {
|
||||
diasHtml += `<td class="th-blue" style="width: 28px;">${d.dia}</td>`;
|
||||
});
|
||||
m.dias.forEach((d: any) => { diasHtml += `<td class="th-blue" style="width: 28px;">${d.dia}</td>`; });
|
||||
diasHtml += `<td class="th-total" style="width: 40px;">Total</td>`;
|
||||
});
|
||||
|
||||
@@ -263,13 +282,12 @@ export default function GestaoRelatorios() {
|
||||
alunosHtml += `<tr class="${idx % 2 === 0 ? 'zebra' : ''}" style="height: 28px;">
|
||||
<td class="td-cell">${r.n_escola}</td>
|
||||
<td class="td-name">${r.aluno_nome}</td>`;
|
||||
|
||||
|
||||
let totalFeitasGlobais = 0;
|
||||
const horasDiarias = parseInt(String(r.horas_diarias || '8').match(/\d+/)?.[0] || '8');
|
||||
|
||||
|
||||
meses.forEach((m: any) => {
|
||||
let totalMesFeitas = 0;
|
||||
|
||||
m.dias.forEach((d: any) => {
|
||||
const p = presencas?.find(x => x.aluno_id === r.aluno_id && x.data === d.dataStr);
|
||||
if (p) {
|
||||
@@ -288,11 +306,10 @@ export default function GestaoRelatorios() {
|
||||
alunosHtml += `<td class="td-cell"></td>`;
|
||||
}
|
||||
});
|
||||
|
||||
totalFeitasGlobais += totalMesFeitas;
|
||||
alunosHtml += `<td class="td-total-cell">${totalMesFeitas}</td>`;
|
||||
});
|
||||
|
||||
|
||||
const faltam = Math.max(0, (r.horas_totais || 400) - totalFeitasGlobais);
|
||||
alunosHtml += `
|
||||
<td class="td-cell" style="font-weight:bold; color: #003049; font-size: 11px;">${totalFeitasGlobais}</td>
|
||||
@@ -311,11 +328,7 @@ export default function GestaoRelatorios() {
|
||||
<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>
|
||||
<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 • <span style="color: #EF4444; font-weight: bold;">[ FI ]</span> Falta Injustificada
|
||||
</div>
|
||||
@@ -326,39 +339,21 @@ export default function GestaoRelatorios() {
|
||||
|
||||
const gerarExcelGeral = async () => {
|
||||
if (relatoriosFiltrados.length === 0) return Alert.alert("Aviso", "Não há alunos para exportar nesta turma.");
|
||||
setGerandoDocumento("excel");
|
||||
setGerandoDocumento("excel");
|
||||
try {
|
||||
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>
|
||||
<meta charset="utf-8" />
|
||||
${getEstilosHTML()}
|
||||
</head>
|
||||
<body>
|
||||
${htmlCorpo}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
<head><meta charset="utf-8" />${getEstilosHTML()}</head>
|
||||
<body>${htmlCorpo}</body></html>`;
|
||||
const fileNameTurma = turmaAtiva ? turmaAtiva.replace(/[^a-zA-Z0-9]/g, '') : 'Geral';
|
||||
const fileUri = FileSystem.documentDirectory + `Mapa_Assiduidade_${fileNameTurma}.xls`;
|
||||
|
||||
await FileSystem.writeAsStringAsync(fileUri, docExcel, { encoding: FileSystem.EncodingType.UTF8 });
|
||||
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(fileUri, {
|
||||
mimeType: 'application/vnd.ms-excel',
|
||||
dialogTitle: 'Exportar Matriz Excel',
|
||||
UTI: 'com.microsoft.excel.xls'
|
||||
});
|
||||
await Sharing.shareAsync(fileUri, { mimeType: 'application/vnd.ms-excel', dialogTitle: 'Exportar Matriz Excel', UTI: 'com.microsoft.excel.xls' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Alert.alert('Erro', 'Falha ao exportar a Matriz Excel.');
|
||||
} finally {
|
||||
setGerandoDocumento(null);
|
||||
}
|
||||
} catch (e) { console.error(e); Alert.alert('Erro', 'Falha ao exportar a Matriz Excel.'); }
|
||||
finally { setGerandoDocumento(null); }
|
||||
};
|
||||
|
||||
const gerarPautaPDF = async () => {
|
||||
@@ -366,147 +361,201 @@ export default function GestaoRelatorios() {
|
||||
setGerandoDocumento("pdf");
|
||||
try {
|
||||
const htmlCorpo = await construirHtmlMatriz();
|
||||
|
||||
const htmlCompletoPDF = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-PT">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
${getEstilosHTML()}
|
||||
<style>
|
||||
@page { size: A4 landscape; margin: 10mm; }
|
||||
body { font-family: 'Segoe UI', Helvetica, sans-serif; margin: 0; padding: 0; background-color: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${htmlCorpo}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const htmlCompletoPDF = `<!DOCTYPE html><html lang="pt-PT"><head><meta charset="UTF-8">${getEstilosHTML()}
|
||||
<style>@page { size: A4 landscape; margin: 10mm; } body { font-family: 'Segoe UI', Helvetica, sans-serif; margin: 0; padding: 0; background-color: #fff; }</style>
|
||||
</head><body>${htmlCorpo}</body></html>`;
|
||||
const { uri } = await Print.printToFileAsync({ html: htmlCompletoPDF });
|
||||
const fileNameTurma = turmaAtiva ? turmaAtiva.replace(/[^a-zA-Z0-9]/g, '') : 'Geral';
|
||||
const newFileUri = `${FileSystem.documentDirectory}Mapa_Assiduidade_${fileNameTurma}.pdf`;
|
||||
|
||||
await FileSystem.moveAsync({ from: uri, to: newFileUri });
|
||||
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Alert.alert('Erro', 'Falha ao gerar o PDF Oficial.');
|
||||
} finally {
|
||||
setGerandoDocumento(null);
|
||||
}
|
||||
if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(newFileUri, { mimeType: 'application/pdf', UTI: 'com.adobe.pdf' });
|
||||
} catch (e) { console.error(e); Alert.alert('Erro', 'Falha ao gerar o PDF Oficial.'); }
|
||||
finally { setGerandoDocumento(null); }
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// 3. EXPORTAR A AUTOAVALIAÇÃO DO ALUNO
|
||||
// =====================================================================
|
||||
// ─── PDF da Autoavaliação (mantido) ────────────────────────────────────────
|
||||
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>
|
||||
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 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 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>
|
||||
|
||||
${footer}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
</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); }
|
||||
};
|
||||
|
||||
// ─── NOVO: PDF da Avaliação da Empresa ─────────────────────────────────────
|
||||
|
||||
const gerarAvaliacaoEmpresaPDF = async (r: any) => {
|
||||
if (r.nota_empresa == null && !r.avaliacao_empresa_detalhes) {
|
||||
Alert.alert('Aviso', 'A empresa ainda não avaliou este aluno.');
|
||||
return;
|
||||
}
|
||||
|
||||
setGerandoDocumento(`emp_${r.id_estagio}`);
|
||||
try {
|
||||
const dadosAvaliacao = r.avaliacao_empresa_detalhes;
|
||||
|
||||
let respostas: Record<string, number> = {};
|
||||
if (dadosAvaliacao?.respostas) {
|
||||
respostas = typeof dadosAvaliacao.respostas === 'string'
|
||||
? JSON.parse(dadosAvaliacao.respostas)
|
||||
: dadosAvaliacao.respostas;
|
||||
}
|
||||
|
||||
const categorias: Record<string, typeof PERGUNTAS_EMP> = {};
|
||||
PERGUNTAS_EMP.forEach(p => {
|
||||
if (!categorias[p.categoria]) categorias[p.categoria] = [];
|
||||
categorias[p.categoria].push(p);
|
||||
});
|
||||
|
||||
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_EMP.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>`;
|
||||
});
|
||||
});
|
||||
|
||||
const notaNum = Number(r.nota_empresa);
|
||||
const positiva = notaNum >= 9.5;
|
||||
const dataAval = dadosAvaliacao?.atualizado_em ? new Date(dadosAvaliacao.atualizado_em).toLocaleDateString('pt-PT') : '—';
|
||||
const observacoes = dadosAvaliacao?.observacoes || '';
|
||||
|
||||
const { header, footer, watermark } = await gerarCabecalhoERodape();
|
||||
const tituloEmp = `<h1 style="color:#003049;margin:0;font-size:18px;font-family:sans-serif;">Ficha de Avaliação da Entidade</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('', tituloEmp);
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html><html lang="pt-PT"><head><meta charset="UTF-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 15mm; }
|
||||
body { font-family: 'Segoe UI', sans-serif; color: #1E293B; font-size: 11px; margin: 0; padding: 0; }
|
||||
.info { width: 100%; border-collapse: collapse; margin: 15px 0; border: 1px solid #CBD5E1; }
|
||||
.info td { padding: 6px 10px; border: 1px solid #CBD5E1; font-size: 11px; }
|
||||
.info td.label { background: #F8FAFC; font-weight: bold; width: 28%; }
|
||||
.section { background: #003049; color: #fff; padding: 6px 10px; font-weight: bold; font-size: 11px; margin-top: 18px; }
|
||||
.tbl { width: 100%; border-collapse: collapse; font-size: 10px; }
|
||||
.tbl td { border: 1px solid #CBD5E1; padding: 6px 8px; }
|
||||
.cat-row { background: #F18721; color: #fff; font-weight: bold; text-transform: uppercase; font-size: 10px; letter-spacing: 0.5px; }
|
||||
.td-text { width: 70%; }
|
||||
.td-score { width: 30%; text-align: center; font-size: 11px; }
|
||||
.nota-box { text-align: center; padding: 18px; border: 2px solid #E2E8F0; background: #F8FAFC; margin-top: 10px; }
|
||||
.nota-num { font-size: 42px; font-weight: 900; color: ${positiva ? '#15803D' : '#B91C1C'}; }
|
||||
.nota-lbl { font-size: 12px; color: #64748B; font-weight: bold; margin-top: 4px; }
|
||||
.obs { border: 1px solid #CBD5E1; padding: 12px; background: #F8FAFC; font-style: italic; min-height: 60px; font-size: 11px; line-height: 1.5; }
|
||||
</style></head><body>
|
||||
${watermark}
|
||||
${headerCompleto}
|
||||
<table class="info">
|
||||
<tr><td class="label">Estagiário(a):</td><td><strong>${r.aluno_nome}</strong></td></tr>
|
||||
<tr><td class="label">Nº Escola / Turma:</td><td>${r.n_escola} · ${r.turma}</td></tr>
|
||||
<tr><td class="label">Entidade de Acolhimento:</td><td><strong>${r.empresa_nome}</strong></td></tr>
|
||||
<tr><td class="label">Tutor(a) Responsável:</td><td>${r.tutor_nome}</td></tr>
|
||||
<tr><td class="label">Data da Avaliação:</td><td>${dataAval}</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="section">Avaliação Qualitativa (1 = Insatisfatório · 5 = Excelente)</div>
|
||||
<table class="tbl">${linhasPerguntas}</table>
|
||||
|
||||
<div class="section">Nota Final Atribuída pela Entidade</div>
|
||||
<div class="nota-box">
|
||||
<div class="nota-num">${notaNum}</div>
|
||||
<div class="nota-lbl">/ 20 valores · ${positiva ? 'POSITIVA' : 'NEGATIVA'}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">Observações da Entidade</div>
|
||||
<div class="obs">${observacoes.trim() ? observacoes.replace(/\n/g,'<br/>') : '<em>Sem observações adicionais.</em>'}</div>
|
||||
|
||||
${footer}
|
||||
</body></html>`;
|
||||
|
||||
const { uri } = await Print.printToFileAsync({ html });
|
||||
const safeName = r.aluno_nome.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const newFileUri = `${FileSystem.documentDirectory}Avaliacao_Empresa_${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.');
|
||||
Alert.alert('Erro', 'Não foi possível gerar a avaliação da empresa.');
|
||||
} finally {
|
||||
setGerandoDocumento(null);
|
||||
}
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// 4. EXPORTAR O DIÁRIO DE BORDO INDIVIDUAL
|
||||
// =====================================================================
|
||||
// ─── Diário de Bordo (mantido) ─────────────────────────────────────────────
|
||||
const gerarSumariosPDF = async (r: any) => {
|
||||
setGerandoDocumento(r.id_estagio);
|
||||
try {
|
||||
let { data: sumarios, error } = await supabase.from('registos_diarios').select('data, tipo, sumario').eq('estagio_id', r.id_estagio).order('data', { ascending: true });
|
||||
if (error) throw error;
|
||||
|
||||
if (!sumarios || sumarios.length === 0) {
|
||||
if (!r.aluno_id) throw new Error("ID do aluno não encontrado.");
|
||||
const { data: presencas, error: presencasErr } = await supabase.from('presencas').select('data, estado, sumario').eq('aluno_id', r.aluno_id).order('data', { ascending: true });
|
||||
if (presencasErr) throw presencasErr;
|
||||
if (presencas && presencas.length > 0) sumarios = presencas.map(p => ({ data: p.data, tipo: p.estado || 'Presença', sumario: p.sumario }));
|
||||
}
|
||||
|
||||
if (!sumarios || sumarios.length === 0) {
|
||||
Alert.alert('Aviso', 'Não foram encontrados registos para este aluno.');
|
||||
setGerandoDocumento(null);
|
||||
return;
|
||||
setGerandoDocumento(null); return;
|
||||
}
|
||||
|
||||
let linhasTabela = '';
|
||||
@@ -518,151 +567,90 @@ export default function GestaoRelatorios() {
|
||||
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>
|
||||
<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: 12mm 15mm; color: #1E293B; font-size: 10px; }
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${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>
|
||||
|
||||
${footer}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
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: 12mm 15mm; color: #1E293B; font-size: 10px; }
|
||||
.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; }
|
||||
</style></head><body>
|
||||
${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>
|
||||
${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}Diario_Bordo_${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 ler os registos.');
|
||||
} finally {
|
||||
setGerandoDocumento(null);
|
||||
}
|
||||
} catch (e) { console.error(e); Alert.alert('Erro', 'Não foi possível ler os registos.'); }
|
||||
finally { setGerandoDocumento(null); }
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
|
||||
<View style={[styles.header, { paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 15 : 35 }]}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => {
|
||||
if (turmaAtiva) setTurmaAtiva(null);
|
||||
else router.back();
|
||||
}}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => { if (turmaAtiva) setTurmaAtiva(null); else router.back(); }}>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>
|
||||
{turmaAtiva ? turmaAtiva : 'Relatórios por Turma'}
|
||||
</Text>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>{turmaAtiva ? turmaAtiva : 'Relatórios por Turma'}</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchRelatorios(true); }} tintColor={cores.azulMarinho} />}
|
||||
>
|
||||
|
||||
{loading && !refreshing ? (
|
||||
<ActivityIndicator size="large" color={cores.azulMarinho} style={{ marginTop: 50 }} />
|
||||
) : !turmaAtiva ? (
|
||||
<View>
|
||||
{cursosAgrupados.map((cursoGrupo, index) => (
|
||||
<View key={index} style={styles.cursoSection}>
|
||||
|
||||
<View style={styles.cursoHeader}>
|
||||
<Ionicons name="folder-open" size={20} color={cores.laranja} />
|
||||
<Text style={[styles.cursoTitle, { color: cores.texto }]}>{cursoGrupo.curso}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.cardsRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count10 > 0 ? 1 : 0.5 }]}
|
||||
activeOpacity={0.7}
|
||||
disabled={cursoGrupo.count10 === 0}
|
||||
onPress={() => setTurmaAtiva(`10º ${cursoGrupo.curso}`)}
|
||||
>
|
||||
<TouchableOpacity style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count10 > 0 ? 1 : 0.5 }]} activeOpacity={0.7} disabled={cursoGrupo.count10 === 0} onPress={() => setTurmaAtiva(`10º ${cursoGrupo.curso}`)}>
|
||||
<Text style={[styles.turmaAno, { color: cores.texto }]}>10º</Text>
|
||||
<Text style={[styles.turmaCount, { color: cores.textoSecundario }]}>{cursoGrupo.count10} Aluno(s)</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count11 > 0 ? 1 : 0.5 }]}
|
||||
activeOpacity={0.7}
|
||||
disabled={cursoGrupo.count11 === 0}
|
||||
onPress={() => setTurmaAtiva(`11º ${cursoGrupo.curso}`)}
|
||||
>
|
||||
<TouchableOpacity style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count11 > 0 ? 1 : 0.5 }]} activeOpacity={0.7} disabled={cursoGrupo.count11 === 0} onPress={() => setTurmaAtiva(`11º ${cursoGrupo.curso}`)}>
|
||||
<Text style={[styles.turmaAno, { color: cores.texto }]}>11º</Text>
|
||||
<Text style={[styles.turmaCount, { color: cores.textoSecundario }]}>{cursoGrupo.count11} Aluno(s)</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count12 > 0 ? 1 : 0.5 }]}
|
||||
activeOpacity={0.7}
|
||||
disabled={cursoGrupo.count12 === 0}
|
||||
onPress={() => setTurmaAtiva(`12º ${cursoGrupo.curso}`)}
|
||||
>
|
||||
<TouchableOpacity style={[styles.turmaCard, { backgroundColor: cores.card, borderColor: cores.borda, opacity: cursoGrupo.count12 > 0 ? 1 : 0.5 }]} activeOpacity={0.7} disabled={cursoGrupo.count12 === 0} onPress={() => setTurmaAtiva(`12º ${cursoGrupo.curso}`)}>
|
||||
<Text style={[styles.turmaAno, { color: cores.texto }]}>12º</Text>
|
||||
<Text style={[styles.turmaCount, { color: cores.textoSecundario }]}>{cursoGrupo.count12} Aluno(s)</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))}
|
||||
|
||||
{cursosAgrupados.length === 0 && (
|
||||
<Text style={{ textAlign: 'center', color: cores.textoSecundario, marginTop: 50 }}>Nenhum estágio em sistema.</Text>
|
||||
<Text style={{ textAlign: 'center', color: cores.textoSecundario, marginTop: 50 }}>Nenhum estágio em sistema.</Text>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<View style={styles.botoesGlobaisContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnGlobal, { backgroundColor: cores.verdeAgua + '30', borderColor: cores.verdeAgua }]}
|
||||
onPress={gerarExcelGeral}
|
||||
disabled={gerandoDocumento === "excel"}
|
||||
>
|
||||
{gerandoDocumento === "excel" ? (
|
||||
<ActivityIndicator size="small" color="#003049" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="grid-outline" size={26} color="#003049" />
|
||||
<Text style={[styles.btnGlobalText, { color: '#003049' }]}>Matriz em Excel</Text>
|
||||
</>
|
||||
<TouchableOpacity style={[styles.btnGlobal, { backgroundColor: cores.verdeAgua + '30', borderColor: cores.verdeAgua }]} onPress={gerarExcelGeral} disabled={gerandoDocumento === "excel"}>
|
||||
{gerandoDocumento === "excel" ? <ActivityIndicator size="small" color="#003049" /> : (
|
||||
<><Ionicons name="grid-outline" size={26} color="#003049" /><Text style={[styles.btnGlobalText, { color: '#003049' }]}>Matriz em Excel</Text></>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnGlobal, { backgroundColor: cores.laranja + '20', borderColor: cores.laranja }]}
|
||||
onPress={gerarPautaPDF}
|
||||
disabled={gerandoDocumento === "pdf"}
|
||||
>
|
||||
{gerandoDocumento === "pdf" ? (
|
||||
<ActivityIndicator size="small" color={cores.laranja} />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="document-text" size={26} color={cores.laranja} />
|
||||
<Text style={[styles.btnGlobalText, { color: cores.laranja }]}>Matriz em PDF</Text>
|
||||
</>
|
||||
<TouchableOpacity style={[styles.btnGlobal, { backgroundColor: cores.laranja + '20', borderColor: cores.laranja }]} onPress={gerarPautaPDF} disabled={gerandoDocumento === "pdf"}>
|
||||
{gerandoDocumento === "pdf" ? <ActivityIndicator size="small" color={cores.laranja} /> : (
|
||||
<><Ionicons name="document-text" size={26} color={cores.laranja} /><Text style={[styles.btnGlobalText, { color: cores.laranja }]}>Matriz em PDF</Text></>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -680,7 +668,6 @@ export default function GestaoRelatorios() {
|
||||
) : (
|
||||
relatoriosFiltrados.map((r, index) => (
|
||||
<View key={index} style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulMarinho }]}>
|
||||
<Text style={{ color: '#FFF', fontWeight: 'bold', fontSize: 18 }}>{r.aluno_nome.charAt(0)}</Text>
|
||||
@@ -693,23 +680,30 @@ export default function GestaoRelatorios() {
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
|
||||
|
||||
{/* 1. AVALIAÇÃO DA EMPRESA */}
|
||||
{/* 1. AVALIAÇÃO DA EMPRESA — agora gera o PDF on-demand */}
|
||||
<View style={styles.moduloBox}>
|
||||
<View style={styles.moduloHeader}>
|
||||
<Text style={[styles.moduloTitle, { color: cores.texto }]}>1. Avaliação da Empresa</Text>
|
||||
{r.nota_empresa ? (
|
||||
{r.nota_empresa != null ? (
|
||||
<Text style={[styles.notaTag, { backgroundColor: cores.azulMarinho + '20', color: cores.azulMarinho }]}>{r.nota_empresa} Val.</Text>
|
||||
) : (
|
||||
<Text style={[styles.notaTag, { backgroundColor: cores.laranja + '20', color: cores.laranja }]}>Pendente</Text>
|
||||
)}
|
||||
</View>
|
||||
{r.pdf_empresa && (
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
|
||||
onPress={() => WebBrowser.openBrowserAsync(r.pdf_empresa)}
|
||||
{r.nota_empresa != null && (
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
|
||||
onPress={() => gerarAvaliacaoEmpresaPDF(r)}
|
||||
disabled={gerandoDocumento === `emp_${r.id_estagio}`}
|
||||
>
|
||||
<Ionicons name="reader-outline" size={20} color={cores.azulMarinho} />
|
||||
<Text style={[styles.textoAcao, { color: cores.azulMarinho }]}>Ver Ficha de Avaliação</Text>
|
||||
{gerandoDocumento === `emp_${r.id_estagio}` ? (
|
||||
<ActivityIndicator size="small" color={cores.azulMarinho} />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="reader-outline" size={20} color={cores.azulMarinho} />
|
||||
<Text style={[styles.textoAcao, { color: cores.azulMarinho }]}>Exportar Avaliação (PDF)</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -725,8 +719,8 @@ export default function GestaoRelatorios() {
|
||||
)}
|
||||
</View>
|
||||
{r.autoavaliacao && (
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
|
||||
onPress={() => gerarAutoavaliacaoPDF(r)}
|
||||
disabled={gerandoDocumento === `auto_${r.id_estagio}`}
|
||||
>
|
||||
@@ -748,8 +742,8 @@ export default function GestaoRelatorios() {
|
||||
<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.borda, backgroundColor: cores.fundo }]}
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAcaoLigeiro, { borderColor: cores.borda, backgroundColor: cores.fundo }]}
|
||||
onPress={() => gerarSumariosPDF(r)}
|
||||
disabled={gerandoDocumento === r.id_estagio}
|
||||
>
|
||||
@@ -763,13 +757,11 @@ export default function GestaoRelatorios() {
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -780,46 +772,46 @@ const styles = StyleSheet.create({
|
||||
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 },
|
||||
|
||||
cursoSection: { marginBottom: 30 },
|
||||
cursoHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15, gap: 8 },
|
||||
cursoTitle: { fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: -0.5 },
|
||||
|
||||
|
||||
cardsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 10 },
|
||||
turmaCard: { flex: 1, paddingVertical: 20, borderRadius: 20, borderWidth: 1, alignItems: 'center', justifyContent: 'center', elevation: 1, shadowOpacity: 0.05, shadowRadius: 5 },
|
||||
turmaAno: { fontSize: 24, fontWeight: '900', marginBottom: 4 },
|
||||
turmaCount: { fontSize: 11, fontWeight: '700' },
|
||||
|
||||
|
||||
botoesGlobaisContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 25, gap: 15 },
|
||||
btnGlobal: { flex: 1, flexDirection: 'column', alignItems: 'center', paddingVertical: 18, borderRadius: 20, borderWidth: 1.5, elevation: 0 },
|
||||
btnGlobalText: { fontSize: 13, fontWeight: '900', marginTop: 8 },
|
||||
|
||||
|
||||
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', letterSpacing: 1 },
|
||||
|
||||
|
||||
card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 20, elevation: 1, shadowOpacity: 0.05, shadowRadius: 10 },
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center' },
|
||||
avatar: { width: 44, height: 44, borderRadius: 22, justifyContent: 'center', alignItems: 'center' },
|
||||
alunoName: { fontSize: 16, fontWeight: '900', letterSpacing: -0.5 },
|
||||
alunoSub: { fontSize: 12, fontWeight: '600', marginTop: 2 },
|
||||
divider: { height: 1, marginVertical: 15 },
|
||||
|
||||
|
||||
moduloBox: { borderBottomWidth: 1, borderBottomColor: '#E2E8F0', paddingBottom: 15, marginBottom: 15 },
|
||||
moduloHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
moduloTitle: { fontSize: 13, fontWeight: '800' },
|
||||
notaTag: { fontSize: 11, fontWeight: '900', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
|
||||
|
||||
btnAcaoLigeiro: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
marginTop: 12,
|
||||
gap: 8
|
||||
|
||||
btnAcaoLigeiro: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
marginTop: 12,
|
||||
gap: 8
|
||||
},
|
||||
textoAcao: { fontSize: 13, fontWeight: '800' }
|
||||
});
|
||||
Reference in New Issue
Block a user