diff --git a/app.json b/app.json index 71b8816..3f8a902 100644 --- a/app.json +++ b/app.json @@ -45,11 +45,12 @@ "photosPermission": "Permitir que a aplicação aceda às tuas fotos para alterar a foto de perfil.", "cameraPermission": "Permitir que a aplicação utilize a câmara para tirar uma foto de perfil." } - ] + ], + "expo-asset" ], "experiments": { "typedRoutes": true, "reactCompiler": true } } -} \ No newline at end of file +} diff --git a/app/Empresas/EmpresaHome.tsx b/app/Empresas/EmpresaHome.tsx index 7694f66..e6daa66 100644 --- a/app/Empresas/EmpresaHome.tsx +++ b/app/Empresas/EmpresaHome.tsx @@ -5,6 +5,8 @@ import { useRouter } from 'expo-router'; import { useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, + Alert, + Dimensions, ScrollView, StatusBar, StyleSheet, @@ -12,26 +14,34 @@ import { TouchableOpacity, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; +const { width } = Dimensions.get('window'); + export default function EmpresaHome() { - const { isDarkMode } = useTheme(); const router = useRouter(); - + const { isDarkMode } = useTheme(); + const insets = useSafeAreaInsets(); + + const [empresaNome, setEmpresaNome] = useState(''); const [loading, setLoading] = useState(true); - const [empresaNome, setEmpresaNome] = useState(''); - const themeStyles = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', + // Paleta EPVC (mantendo a consistência com o Prof) + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', texto: isDarkMode ? '#F8FAFC' : '#1E293B', textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', + laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - azul: '#2390a6', - laranja: '#dd8707', - verde: '#10B981', vermelho: '#EF4444', }), [isDarkMode]); @@ -46,7 +56,7 @@ export default function EmpresaHome() { .eq('user_id', user.id) .single(); - if (empresa) { + if (empresa && empresa.nome) { setEmpresaNome(empresa.nome); } } catch (error) { @@ -58,127 +68,168 @@ export default function EmpresaHome() { useFocusEffect(useCallback(() => { fetchEmpresaInfo(); }, [])); - return ( - - - - {/* CABEÇALHO */} - - - Painel da Entidade - {loading ? ( - - ) : ( - - {empresaNome || 'A carregar...'} - - )} - - supabase.auth.signOut().then(() => router.replace('/'))}> - - - + const handleLogout = () => { + Alert.alert('Terminar Sessão', 'Tens a certeza que pretendes sair?', [ + { text: 'Cancelar', style: 'cancel' }, + { text: 'Sair', style: 'destructive', onPress: () => supabase.auth.signOut().then(() => router.replace('/')) } + ]); + }; + return ( + + + - {/* CARTÃO DE DESTAQUE - PEDIDOS PENDENTES */} - AÇÃO IMEDIATA - router.push('/Empresas/pedidos')} - > - - - - - - Validações Pendentes - Aprovar presenças e sumários + {/* Cabeçalho */} + + + + + + Painel da Entidade Parceira + + {loading ? ( + + ) : ( + + Olá, {empresaNome || 'Empresa'} + + )} + Gestão de Estagiários EPVC - - - - {/* SECÇÃO GESTÃO - GRELHA 2 COLUNAS */} - GESTÃO DE ESTÁGIOS - - - router.push('/Empresas/alunos')} - > - - - - Alunos - Gerir estagiários - - - router.push('/Empresas/avaliacoesEmpresa')} - > - - - - Avaliações - Avaliar estágios - - {/* DEFINIÇÕES - CARTÃO LARGO INFERIOR */} + {/* Criar Registo / Ação Imediata (Hero Card) */} router.push('/Empresas/definicoesEmpresa')} + style={[styles.heroCard, { backgroundColor: cores.laranja }]} + activeOpacity={0.85} + onPress={() => router.push('/Empresas/pedidos')} > - - + + + + Validações + Aprovar presenças e sumários dos alunos + + + + - - Definições da Conta - Ajustar dados da empresa e segurança - - + {/* SECÇÃO: Gestão de Estágios */} + + Gestão de Estágios + + router.push('/Empresas/alunos')} cores={cores} corDestaque={cores.azul} /> + router.push('/Empresas/avaliacoesEmpresa')} cores={cores} corDestaque={cores.azul} /> + + + + {/* SECÇÃO: Sistema */} + + Sistema + + router.push('/Empresas/definicoesEmpresa')} cores={cores} corDestaque={cores.textoSecundario} /> + + + + + + + Estágios+ • Portal da Empresa + + ); } +// COMPONENTE DE CARTÃO REFORMULADO +function MenuCard({ icon, title, subtitle, onPress, cores, corDestaque, fullWidth = false }: any) { + const { isDarkMode } = useTheme(); + + return ( + + + + + + + + + {title} + {subtitle} + + + ); +} + const styles = StyleSheet.create({ - safeArea: { flex: 1 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 10, paddingBottom: 15 }, - greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, - title: { fontSize: 24, fontWeight: '900', marginTop: 2 }, - logoutBtn: { width: 45, height: 45, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + content: { padding: 20 }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, - sectionLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 1.2, marginBottom: 12, marginLeft: 5 }, + // Header + header: { marginBottom: 25, marginTop: 10 }, + headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, + badgeEpvc: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 8 }, + badgeTxt: { fontSize: 10, fontWeight: '900', letterSpacing: 1, marginLeft: 4 }, + name: { fontSize: 26, fontWeight: '900', letterSpacing: -0.5 }, + subtitle: { fontSize: 14, fontWeight: '500', marginTop: 2 }, + avatarMini: { width: 56, height: 56, borderRadius: 20, justifyContent: 'center', alignItems: 'center', borderWidth: 2 }, + avatarTxt: { fontSize: 24, fontWeight: '900' }, - heroCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, borderRadius: 24, borderWidth: 1, borderLeftWidth: 6, marginBottom: 25, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.05, shadowRadius: 10 }, - heroContent: { flexDirection: 'row', alignItems: 'center', flex: 1 }, - iconWrapperLarge: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginRight: 15 }, - heroTextContainer: { flex: 1, paddingRight: 10 }, - heroTitle: { fontSize: 19, fontWeight: '900', marginBottom: 4 }, - heroDesc: { fontSize: 13, fontWeight: '600' }, + // Hero Card (Ação Imediata) + heroCard: { borderRadius: 28, padding: 24, minHeight: 140, justifyContent: 'flex-end', overflow: 'hidden', elevation: 8, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, marginBottom: 30 }, + heroWatermark: { position: 'absolute', right: -20, top: -20, opacity: 0.2, transform: [{ rotate: '15deg' }] }, + heroContent: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' }, + heroTitle: { color: '#FFF', fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, + heroSubtitle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 13, fontWeight: '600', marginTop: 4 }, + heroBtn: { width: 44, height: 44, backgroundColor: '#FFF', borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, - gridContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 }, - gridCard: { width: '48%', padding: 20, borderRadius: 24, borderWidth: 1, alignItems: 'flex-start', elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 }, - iconWrapper: { padding: 12, borderRadius: 16, marginBottom: 16 }, - gridCardTitle: { fontSize: 16, fontWeight: '900', marginBottom: 4 }, - gridCardDesc: { fontSize: 12, fontWeight: '600' }, + // Secções e Grid + sectionContainer: { marginBottom: 25 }, + sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12, marginLeft: 4 }, + grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, + + // Cartões Menores + card: { + borderRadius: 24, + padding: 18, + marginBottom: 16, + borderWidth: 1, + overflow: 'hidden', + elevation: 2, + shadowColor: '#000', + shadowOpacity: 0.04, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 } + }, + cardWatermark: { position: 'absolute', right: -15, bottom: -15, transform: [{ rotate: '-10deg' }] }, + iconWrapper: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginBottom: 16 }, + cardTextContainer: { marginTop: 'auto' }, + cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600' }, - rowCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, borderWidth: 1, marginTop: 5 }, - iconWrapperSmall: { width: 45, height: 45, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginRight: 15 }, - rowTextContainer: { flex: 1 }, - rowTitle: { fontSize: 15, fontWeight: '800', marginBottom: 2 }, - rowDesc: { fontSize: 12, fontWeight: '600' } + // Footer + footer: { marginTop: 20, alignItems: 'center', paddingBottom: 20 }, + footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 } }); \ No newline at end of file diff --git a/app/Empresas/alunos.tsx b/app/Empresas/alunos.tsx index d88cf9c..e79da5c 100644 --- a/app/Empresas/alunos.tsx +++ b/app/Empresas/alunos.tsx @@ -4,17 +4,17 @@ import { useFocusEffect } from '@react-navigation/native'; import { useRouter } from 'expo-router'; import { useCallback, useMemo, useState } from 'react'; import { - ActivityIndicator, - Alert, - Platform, - RefreshControl, - SafeAreaView, - ScrollView, - StatusBar, - StyleSheet, - Text, - TouchableOpacity, - View + ActivityIndicator, + Alert, + Platform, + RefreshControl, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; @@ -55,7 +55,6 @@ export default function GestaoAlunos() { return; } - // 🐅 GRAÇAS AO TIGRE, BASTA LER A COLUNA horas_concluidas DIRETO DA BASE DE DADOS! const { data: estagios, error } = await supabase .from('estagios') .select(` @@ -72,7 +71,6 @@ export default function GestaoAlunos() { if (error) throw error; const formatados = estagios?.map((estagio: any) => { - // 🟢 TRUQUE ANTI-ERRO: Tira o aluno da lista se o Supabase mandar em formato Array const alunoObj = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos; return { @@ -82,7 +80,7 @@ export default function GestaoAlunos() { data_inicio: estagio.data_inicio, data_fim: estagio.data_fim, horas_totais: estagio.horas_totais || 0, - horas_concluidas: estagio.horas_concluidas || 0, // <-- LIDO DIRETO DA TABELA! + horas_concluidas: estagio.horas_concluidas || 0, }; }) || []; @@ -159,8 +157,13 @@ export default function GestaoAlunos() { const progressoPercent = aluno.horas_totais > 0 ? (aluno.horas_concluidas / aluno.horas_totais) * 100 : 0; return ( - router.push({ + pathname: '/Empresas/detalhesAluno', + params: { estagio_id: aluno.id_estagio, aluno_nome: aluno.aluno_nome } + })} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: themeStyles.azul }]} > @@ -199,7 +202,7 @@ export default function GestaoAlunos() { - + ); }) )} diff --git a/app/Empresas/avaliacoesEmpresa.tsx b/app/Empresas/avaliacoesEmpresa.tsx index e69de29..d2d82aa 100644 --- a/app/Empresas/avaliacoesEmpresa.tsx +++ b/app/Empresas/avaliacoesEmpresa.tsx @@ -0,0 +1,174 @@ +// app/Empresas/avaliacoesEmpresa.tsx +import { Ionicons } from '@expo/vector-icons'; +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 +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; +import { useTheme } from '../../themecontext'; + +export default function AvaliacoesEmpresaLista() { + const router = useRouter(); + const { isDarkMode } = useTheme(); + + const [estagios, setEstagios] = useState([]); + const [loading, setLoading] = useState(true); + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + azul: '#2390a6', + verde: '#10B981', + laranja: '#E38E00', + }), [isDarkMode]); + + const fetchAlunos = async () => { + setLoading(true); + try { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return; + + const { data: empresa } = await supabase + .from('empresas') + .select('id') + .eq('user_id', user.id) + .single(); + + if (!empresa) return; + + // Vai buscar os estagios desta empresa e cruza com os nomes dos alunos + const { data, error } = await supabase + .from('estagios') + .select(` + id, + nota_final, + alunos (id, nome) + `) + .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; + + 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 + }; + }); + setEstagios(listaFormatada); + } + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + useFocusEffect(useCallback(() => { fetchAlunos(); }, [])); + + const renderItem = ({ item }: any) => ( + router.push({ + pathname: '/Empresas/fichaAvaliacao', + params: { estagio_id: item.estagio_id, aluno_nome: item.aluno_nome, nota_atual: item.nota } + })} + > + + + + + + + {item.aluno_nome} + + {item.avaliado ? '✓ Avaliação Concluída' : 'Aguardando Avaliação'} + + + + + {item.avaliado ? ( + + {item.nota} / 20 + + ) : ( + + )} + + + ); + + return ( + + + + + router.back()}> + + + Avaliar Alunos + + + + {loading ? ( + + + + ) : ( + item.estagio_id} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + ListEmptyComponent={ + + + Nenhum estagiário associado à sua entidade neste momento. + + } + renderItem={renderItem} + /> + )} + + ); +} + +const styles = StyleSheet.create({ + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 10, paddingBottom: 20 }, + btnVoltar: { padding: 5, marginLeft: -5 }, + headerTitle: { fontSize: 20, fontWeight: '900' }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + listContent: { paddingHorizontal: 20, paddingBottom: 40 }, + + card: { padding: 18, borderRadius: 24, borderWidth: 1, marginBottom: 15, elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 }, + cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, + iconBox: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + alunoName: { fontSize: 16, fontWeight: '800', marginBottom: 2 }, + statusText: { fontSize: 12, fontWeight: '700' }, + + gradeBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12 }, + gradeText: { fontSize: 14, fontWeight: '900' }, + + emptyBox: { alignItems: 'center', marginTop: 60, paddingHorizontal: 30 }, + emptyText: { textAlign: 'center', marginTop: 15, fontSize: 15, fontWeight: '500', lineHeight: 22 } +}); \ No newline at end of file diff --git a/app/Empresas/detalhesAluno.tsx b/app/Empresas/detalhesAluno.tsx new file mode 100644 index 0000000..50b9784 --- /dev/null +++ b/app/Empresas/detalhesAluno.tsx @@ -0,0 +1,252 @@ +// app/Empresa/detalhesAluno.tsx +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 +} from 'react-native'; +import { supabase } from '../../lib/supabase'; +import { useTheme } from '../../themecontext'; + +export default function DetalhesAlunoEmpresa() { + const { isDarkMode } = useTheme(); + const router = useRouter(); + const params = useLocalSearchParams(); + + const estagio_id = Array.isArray(params.estagio_id) ? params.estagio_id[0] : params.estagio_id; + + const [estagio, setEstagio] = useState(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#0D2235', + textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + azulMarinho: '#003049', + verdeAgua: '#71BEB3', + laranja: '#F18721', + }), [isDarkMode]); + + const fetchDetalhes = async (isManualRefresh = false) => { + if (!estagio_id) return; + if (!isManualRefresh) setLoading(true); + + try { + // 1. Buscar dados do Estágio e do Aluno (Removido o campo cargo) + 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) + `) + .eq('id', estagio_id) + .single(); + + if (error) throw error; + + const alunoData = Array.isArray(data.alunos) ? data.alunos[0] : data.alunos; + + // 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' }; + + if (alunoData?.profile_id) { + const { data: profile, error: profError } = await supabase + .from('profiles') + .select('telefone, email, residencia, data_nascimento') + .eq('id', alunoData.profile_id) + .single(); + + if (!profError && profile) { + infoExtra = { + telefone: profile.telefone || 'N/A', + email: profile.email || 'N/A', + residencia: profile.residencia || 'N/A', + d_nasc: profile.data_nascimento || 'N/A' + }; + } + } + + setEstagio({ ...data, aluno: alunoData, infoExtra }); + + } catch (error) { + console.error(error); + Alert.alert('Erro', 'Falha ao carregar dossiê do aluno.'); + } finally { + if (!isManualRefresh) setLoading(false); + setRefreshing(false); + } + }; + + useFocusEffect(useCallback(() => { fetchDetalhes(); }, [estagio_id])); + + const formatarData = (dataStr: string) => { + if (!dataStr || dataStr === 'N/A') return 'N/A'; + const d = new Date(dataStr); + return d.toLocaleDateString('pt-PT'); + }; + + if (loading && !refreshing) { + return ( + + + + ); + } + + const progresso = estagio?.horas_totais > 0 ? (estagio.horas_concluidas / estagio.horas_totais) * 100 : 0; + + return ( + + + + + router.back()}> + Dossiê do Estagiário + + + + { setRefreshing(true); fetchDetalhes(true); }} />} + > + {/* CABEÇALHO DE IDENTIFICAÇÃO */} + + + {estagio.aluno?.nome?.charAt(0) || '?'} + + {estagio.aluno?.nome} + {estagio.aluno?.turma_curso} + + + {/* INFO ACADÉMICA */} + Informação Académica + + + + Nº ESCOLA + {estagio.aluno?.n_escola || '--'} + + + ANO LETIVO + {estagio.aluno?.ano || '--'}º Ano + + + + + {/* CONTACTOS E MORADA */} + Contactos e Localização + + + + {estagio.infoExtra.telefone} + + + + {estagio.infoExtra.email} + + + + {estagio.infoExtra.residencia} + + + + Nascido a: {formatarData(estagio.infoExtra.d_nasc)} + + + + {/* PROGRESSO */} + Estado do Estágio + + + HORAS REALIZADAS + {estagio.horas_concluidas}h / {estagio.horas_totais}h + + + + + + + DATA INÍCIO + {formatarData(estagio.data_inicio)} + + + FIM PREVISTO + {formatarData(estagio.data_fim)} + + + + + {/* AVALIAÇÃO */} + Documentação Oficial + {estagio.nota_final ? ( + WebBrowser.openBrowserAsync(estagio.avaliacao_url)} + > + + Estagiário Avaliado + Classificação: {estagio.nota_final} Valores + + + + ) : ( + router.push({ + pathname: '/Empresas/fichaAvaliacao', + params: { estagio_id: estagio.id, aluno_nome: estagio.aluno?.nome } + })} + > + + Realizar Avaliação Final + + )} + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { flex: 1 }, + centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 }, + headerTitle: { fontSize: 18, fontWeight: '900' }, + scrollContent: { padding: 20, paddingBottom: 40 }, + card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 15 }, + avatarCirculo: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 10 }, + avatarLetra: { color: '#FFF', fontSize: 28, fontWeight: 'bold' }, + nomeAluno: { fontSize: 22, fontWeight: '900', textAlign: 'center' }, + subAnotacao: { fontSize: 14, fontWeight: '700', textAlign: 'center', marginTop: 2 }, + sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 8, marginLeft: 5, letterSpacing: 1 }, + gridInfo: { flexDirection: 'row', justifyContent: 'space-between' }, + gridItem: { flex: 1 }, + labelMini: { fontSize: 10, color: '#94A3B8', fontWeight: '800', marginBottom: 4 }, + valorMedio: { fontSize: 15, fontWeight: '700' }, + valorData: { fontSize: 13, fontWeight: '700' }, + linhaDetalhe: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, + textoDetalhe: { marginLeft: 12, fontSize: 14, fontWeight: '500' }, + 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 }, + textoBranco: { color: '#FFF', fontSize: 14, fontWeight: '600' }, + notaTexto: { color: '#FFF', fontSize: 18, fontWeight: '900' }, + btnAvaliar: { padding: 18, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 10 }, + btnTexto: { color: '#FFF', fontSize: 16, fontWeight: '900' } +}); \ No newline at end of file diff --git a/app/Empresas/fichaAvaliacao.tsx b/app/Empresas/fichaAvaliacao.tsx new file mode 100644 index 0000000..020b123 --- /dev/null +++ b/app/Empresas/fichaAvaliacao.tsx @@ -0,0 +1,459 @@ +// app/Empresas/fichaAvaliacao.tsx +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 * as Sharing from 'expo-sharing'; +import { useMemo, useState } from 'react'; +import { + ActivityIndicator, + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; +import { useTheme } from '../../themecontext'; + +export default function FichaAvaliacao() { + const router = useRouter(); + const { isDarkMode } = useTheme(); + const params = useLocalSearchParams(); + + const estagio_id = Array.isArray(params.estagio_id) ? params.estagio_id[0] : params.estagio_id; + const aluno_nome = Array.isArray(params.aluno_nome) ? params.aluno_nome[0] : params.aluno_nome; + + const [loading, setLoading] = useState(false); + + // As 10 Perguntas Oficiais das Escolas Profissionais + const [criterios, setCriterios] = useState({ + assiduidade: 0, + relacionamento: 0, + responsabilidade: 0, + iniciativa: 0, + adaptacao: 0, + conhecimentos: 0, + qualidade: 0, + empenho: 0, + equipamentos: 0, + seguranca: 0, + }); + + const [notaFinal, setNotaFinal] = useState(''); + const [observacoes, setObservacoes] = useState(''); + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#0D2235', + textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + azulMarinho: '#003049', + verdeAgua: '#71BEB3', + laranja: '#F18721', + }), [isDarkMode]); + + const 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}`; + } catch (e) { + console.error("Erro no Base64:", e); + return ""; + } + }; + + const ClassificacaoRow = ({ label, field }: { label: string, field: keyof typeof criterios }) => ( + + {label} + + {[1, 2, 3, 4, 5].map((num) => { + const selecionado = criterios[field] === num; + return ( + setCriterios({ ...criterios, [field]: num })} + style={[ + styles.botaoNota, + { + borderColor: selecionado ? cores.azulMarinho : cores.borda, + backgroundColor: selecionado ? cores.azulMarinho : cores.card, + } + ]} + > + + {num} + + + ); + })} + + + ); + + const submeterAvaliacao = async () => { + const faltamRespostas = Object.values(criterios).some(val => val === 0); + if (faltamRespostas) { + Alert.alert('Aviso', 'Preencha todos os 10 parâmetros antes de gerar o PDF.'); + return; + } + + const notaNum = parseInt(notaFinal); + if (isNaN(notaNum) || notaNum < 0 || notaNum > 20) { + Alert.alert('Erro', 'Nota final inválida (0-20).'); + return; + } + + setLoading(true); + + try { + // 1. DADOS DO SUPABASE: Substituído "cargo" por "horas_totais" no destaque + const { data: infoEstagio, error: infoError } = await supabase + .from('estagios') + .select(` + data_inicio, + data_fim, + horas_totais, + empresas (nome, tutor_nome), + alunos (nome, n_escola, turma_curso) + `) + .eq('id', estagio_id) + .single(); + + if (infoError) console.warn("Erro a buscar dados:", infoError); + + const empresaData = Array.isArray(infoEstagio?.empresas) ? infoEstagio?.empresas[0] : infoEstagio?.empresas; + const alunoData = Array.isArray(infoEstagio?.alunos) ? infoEstagio?.alunos[0] : infoEstagio?.alunos; + + const nomeEmpresa = empresaData?.nome || 'Não definida'; + const tutorEmpresa = empresaData?.tutor_nome || 'N/A'; + + const nomeAlunoExtracted = alunoData?.nome || aluno_nome || 'N/A'; + const cursoAluno = alunoData?.turma_curso || 'N/A'; + const numeroAluno = alunoData?.n_escola || '--'; + + const horasTotais = infoEstagio?.horas_totais ? `${infoEstagio.horas_totais} Horas` : 'N/A'; + const dataInicioFormatada = infoEstagio?.data_inicio ? new Date(infoEstagio.data_inicio).toLocaleDateString('pt-PT') : 'N/A'; + const dataFimFormatada = infoEstagio?.data_fim ? new Date(infoEstagio.data_fim).toLocaleDateString('pt-PT') : 'N/A'; + + const logoEPVC_b64 = await getBase64Image(require('../../assets/images/logoepvc2.png')); + const logoEstagios_b64 = await getBase64Image(require('../../assets/images/logo.png')); + const bannerEU_b64 = await getBase64Image(require('../../assets/images/logoepvc.png')); + + const htmlContent = ` + + + + + + + + + + + + + + +
+

Ficha de Avaliação de Estágio

+

Formação em Contexto de Trabalho

+
+ + + + + + + + + + + + + + + + + + + + +
Estagiário:${nomeAlunoExtracted} (Nº ${numeroAluno})Turma/Curso:${cursoAluno}
Entidade:${nomeEmpresa}Tutor(a):${tutorEmpresa}
Carga Horária:${horasTotais}Período:${dataInicioFormatada} a ${dataFimFormatada}
+ +
I. Parâmetros Comportamentais
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Critérios de AvaliaçãoClass.
1. Assiduidade e PontualidadeCumprimento de horários e justificação de ausências.${criterios.assiduidade}
2. Relacionamento InterpessoalIntegração na equipa e trato com superiores e colegas.${criterios.relacionamento}
3. Responsabilidade e OrganizaçãoCuidado com o material, posto de trabalho e planeamento.${criterios.responsabilidade}
4. Iniciativa e AutonomiaAção proativa e resolução de problemas sem supervisão.${criterios.iniciativa}
5. Adaptação a Novas TarefasFacilidade e rapidez de aprendizagem perante novos desafios.${criterios.adaptacao}
+ +
II. Parâmetros Técnicos e Profissionais
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Critérios de AvaliaçãoClass.
6. Aplicação de ConhecimentosUtilização prática dos conhecimentos adquiridos no curso.${criterios.conhecimentos}
7. Qualidade e RigorAtenção ao detalhe, brio profissional e ausência de erros.${criterios.qualidade}
8. Interesse e EmpenhoMotivação, dedicação e vontade contínua de evoluir.${criterios.empenho}
9. Uso de Equipamentos e FerramentasDestreza, manuseamento correto e cuidado técnico.${criterios.equipamentos}
10. Segurança e HigieneCumprimento estrito das normas de segurança no trabalho.${criterios.seguranca}
+ +
III. Parecer Global da Entidade
+
+ ${observacoes ? observacoes.replace(/\n/g, '
') : 'Nenhum parecer qualitativo submetido.'} +
+ +
+
+ CLASSIFICAÇÃO FINAL ATRIBUÍDA: ${notaFinal} / 20 +
+
+ + + + + + `; + + const { uri } = await Print.printToFileAsync({ html: htmlContent }); + + const fileName = `avaliacao_${estagio_id}_${Date.now()}.pdf`; + const formData = new FormData(); + formData.append('file', { + uri: Platform.OS === 'android' ? uri : uri.replace('file://', ''), + name: fileName, + type: 'application/pdf', + } as any); + + const { error: uploadError } = await supabase.storage.from('avaliacoes').upload(fileName, formData); + if (uploadError) throw uploadError; + + const { data: urlData } = supabase.storage.from('avaliacoes').getPublicUrl(fileName); + const { error: dbError } = await supabase + .from('estagios') + .update({ nota_final: notaNum, avaliacao_url: urlData.publicUrl }) + .eq('id', estagio_id); + + if (dbError) throw dbError; + + Alert.alert('Sucesso', 'Ficha oficial gerada com sucesso e anexada ao processo do aluno.'); + if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(uri); + router.back(); + + } catch (e) { + console.error(e); + Alert.alert('Erro', 'Ocorreu uma falha crítica na geração do PDF.'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + router.back()} style={{ padding: 5 }}> + Ficha de Avaliação + + + + + + + + + {aluno_nome} + Avalie os 10 parâmetros oficiais. + + + + Critérios Comportamentais + + + + + + + + + Critérios Técnicos + + + + + + + + + Classificação Final (0-20) + + + Nota Quantitativa + + + + + Parecer Qualitativo + + + + + + {loading ? : Finalizar e Gerar Documento} + + + + + + ); +} + +const styles = StyleSheet.create({ + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 }, + headerTitle: { fontSize: 20, fontWeight: '900' }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, borderWidth: 1, marginBottom: 25, gap: 12 }, + alunoCardText: { fontSize: 17, fontWeight: '900' }, + sectionAppTitle: { fontSize: 13, fontWeight: '900', color: '#64748B', marginBottom: 12, marginLeft: 5, textTransform: 'uppercase' }, + card: { padding: 20, paddingBottom: 5, borderRadius: 24, borderWidth: 1, marginBottom: 30 }, + criterioContainer: { marginBottom: 20 }, + criterioLabel: { fontSize: 14, fontWeight: '700', marginBottom: 12 }, + botoesContainer: { flexDirection: 'row', justifyContent: 'space-between' }, + botaoNota: { width: 48, height: 48, borderRadius: 14, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center' }, + botaoTexto: { fontSize: 18, fontWeight: '800' }, + notaCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 30 }, + notaLabel: { fontSize: 16, fontWeight: '900' }, + gradeInput: { fontSize: 24, fontWeight: '900', borderWidth: 1.5, borderRadius: 16, width: 80, textAlign: 'center', paddingVertical: 12 }, + textArea: { borderRadius: 24, borderWidth: 1, padding: 20, minHeight: 120, fontSize: 15 }, + footerBtnContainer: { padding: 20, borderTopWidth: 1, borderColor: 'rgba(0,0,0,0.05)' }, + btnSubmit: { paddingVertical: 18, borderRadius: 20, alignItems: 'center', justifyContent: 'center', elevation: 3 }, + btnSubmitText: { color: '#FFF', fontSize: 17, fontWeight: '800' } +}); \ No newline at end of file diff --git a/assets/images/logoepvc.png b/assets/images/logoepvc.png new file mode 100644 index 0000000..7c2f095 Binary files /dev/null and b/assets/images/logoepvc.png differ diff --git a/assets/images/logoepvc2.png b/assets/images/logoepvc2.png new file mode 100644 index 0000000..c6e975d Binary files /dev/null and b/assets/images/logoepvc2.png differ diff --git a/package-lock.json b/package-lock.json index bd47bb6..73954bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,10 @@ "@supabase/supabase-js": "^2.91.0", "base64-arraybuffer": "^1.0.2", "expo": "~54.0.27", + "expo-asset": "~12.0.13", "expo-constants": "~18.0.11", "expo-document-picker": "~14.0.8", - "expo-file-system": "~19.0.21", + "expo-file-system": "~19.0.22", "expo-font": "~14.0.10", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", @@ -26,6 +27,7 @@ "expo-linear-gradient": "~15.0.8", "expo-linking": "~8.0.10", "expo-location": "~19.0.8", + "expo-print": "~15.0.8", "expo-router": "~6.0.17", "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.12", @@ -6487,13 +6489,13 @@ } }, "node_modules/expo-asset": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz", - "integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==", + "version": "12.0.13", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz", + "integrity": "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.8", - "expo-constants": "~18.0.12" + "expo-constants": "~18.0.13" }, "peerDependencies": { "expo": "*", @@ -6525,9 +6527,9 @@ } }, "node_modules/expo-file-system": { - "version": "19.0.21", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", - "integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==", + "version": "19.0.22", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.22.tgz", + "integrity": "sha512-l9pgahSc7sJD0bP9vBNeXvZjy8QKDpVHVxWmei/ESQOrzmoj5BidziqLVsyZdxsi+PfdbTtttLTAmddH/JafYA==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -6668,6 +6670,16 @@ "react-native": "*" } }, + "node_modules/expo-print": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-print/-/expo-print-15.0.8.tgz", + "integrity": "sha512-4O0Qzm0On5AmJIl9d+BT+ieTipFp658nHI4aX7vKEFPfj3dfQxG6rDJJpca+rrc9c4Ha8ZFYGvxJG5+4lFq2Pw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-router": { "version": "6.0.17", "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.17.tgz", diff --git a/package.json b/package.json index cce23dd..08a777f 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ "@supabase/supabase-js": "^2.91.0", "base64-arraybuffer": "^1.0.2", "expo": "~54.0.27", + "expo-asset": "~12.0.13", "expo-constants": "~18.0.11", "expo-document-picker": "~14.0.8", - "expo-file-system": "~19.0.21", + "expo-file-system": "~19.0.22", "expo-font": "~14.0.10", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", @@ -29,6 +30,7 @@ "expo-linear-gradient": "~15.0.8", "expo-linking": "~8.0.10", "expo-location": "~19.0.8", + "expo-print": "~15.0.8", "expo-router": "~6.0.17", "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.12",