From 8bf4fac5ed4890562d5837024d0cb299a422bbff Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Thu, 23 Apr 2026 10:42:46 +0100 Subject: [PATCH] atualizacao --- app/Aluno/perfil.tsx | 421 +++++++++++++++++++++++++++---------------- 1 file changed, 263 insertions(+), 158 deletions(-) diff --git a/app/Aluno/perfil.tsx b/app/Aluno/perfil.tsx index dfcd548..93627e1 100644 --- a/app/Aluno/perfil.tsx +++ b/app/Aluno/perfil.tsx @@ -3,34 +3,40 @@ import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { - ActivityIndicator, Animated, - ScrollView, StatusBar, - StyleSheet, Text, TextInput, TouchableOpacity, View + ActivityIndicator, + Animated, + Dimensions, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../themecontext'; import { supabase } from '../lib/supabase'; +const { width } = Dimensions.get('window'); + export default function PerfilAluno() { const { isDarkMode } = useTheme(); const router = useRouter(); const insets = useSafeAreaInsets(); + // --- ESTADOS --- const [loading, setLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); const [perfil, setPerfil] = useState(null); + const [estagio, setEstagio] = useState(null); + const [saving, setSaving] = useState(false); + // --- ANIMAÇÕES --- const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); const alertOpacity = useMemo(() => new Animated.Value(0), []); - - const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => { - setAlertConfig({ msg, type }); - Animated.sequence([ - Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }), - Animated.delay(3000), - Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) - ]).start(() => setAlertConfig(null)); - }, []); + const fadeAnim = useMemo(() => new Animated.Value(0), []); const azulPetroleo = '#2390a6'; @@ -45,20 +51,19 @@ export default function PerfilAluno() { vermelho: '#EF4444', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', verde: '#10B981', + sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.05)', }), [isDarkMode]); - const formatarParaExibir = (data: string) => { - if (!data) return ''; - const [ano, mes, dia] = data.split('-'); - return `${dia}-${mes}-${ano}`; - }; - - const formatarParaSalvar = (data: string) => { - if (!data || data.length < 10) return null; - const [dia, mes, ano] = data.split('-'); - return `${ano}-${mes}-${dia}`; - }; + const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => { + setAlertConfig({ msg, type }); + Animated.sequence([ + Animated.timing(alertOpacity, { toValue: 1, duration: 400, useNativeDriver: true }), + Animated.delay(3000), + Animated.timing(alertOpacity, { toValue: 0, duration: 400, useNativeDriver: true }) + ]).start(() => setAlertConfig(null)); + }, []); + // --- LÓGICA DE FORMATAÇÃO --- const aplicarMascaraData = (text: string) => { const cleaned = text.replace(/\D/g, ''); let formatted = cleaned; @@ -67,214 +72,314 @@ export default function PerfilAluno() { return formatted; }; - const carregarDados = async () => { + // --- CARREGAMENTO DE DADOS --- + const buscarDados = async () => { try { setLoading(true); const { data: { user } } = await supabase.auth.getUser(); if (!user) return; - const { data: profile, error } = await supabase + // Consulta à Tabela Profiles + const { data: pData, error: pError } = await supabase .from('profiles') .select('*') .eq('id', user.id) .single(); + + if (pError) throw pError; - if (error) throw error; - - if (profile?.data_nascimento) { - profile.data_nascimento = formatarParaExibir(profile.data_nascimento); + // Consulta à Tabela Alunos + let aData = {}; + if (pData.n_escola) { + const { data: alunoRes } = await supabase + .from('alunos') + .select('ano, n_escola, nome, turma_curso') + .eq('n_escola', pData.n_escola) + .single(); + if (alunoRes) aData = alunoRes; } - setPerfil({ ...profile, email: user.email }); - } catch (e) { - showAlert('Erro ao carregar perfil.', 'error'); + // Consulta à Tabela Estágios + const { data: eData } = await supabase + .from('estagios') + .select('*, empresas(nome, tutor_nome)') + .eq('aluno_id', user.id) + .single(); + + setPerfil({ ...pData, ...aData, email: user.email }); + setEstagio(eData); + + Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start(); + } catch (err) { + showAlert('Erro ao sincronizar com a base de dados.', 'error'); } finally { setLoading(false); } }; - useEffect(() => { carregarDados(); }, []); + useEffect(() => { buscarDados(); }, []); - const salvarPerfil = async () => { + const salvarDados = async () => { try { - const dataBD = formatarParaSalvar(perfil.data_nascimento); + setSaving(true); const { error } = await supabase.from('profiles').update({ nome: perfil.nome, telefone: perfil.telefone, residencia: perfil.residencia, - data_nascimento: dataBD, - curso: perfil.curso, - n_escola: perfil.n_escola + idade: perfil.idade, + data_nascimento: perfil.data_nascimento }).eq('id', perfil.id); if (error) throw error; + setIsEditing(false); - showAlert('Perfil guardado!', 'success'); + showAlert('Perfil atualizado!', 'success'); + buscarDados(); } catch (e) { - showAlert('Erro ao salvar. Verifica os campos.', 'error'); + showAlert('Falha ao guardar alterações.', 'error'); + } finally { + setSaving(false); } }; - const terminarSessao = async () => { - await supabase.auth.signOut(); - router.replace('/'); - }; - - if (loading) return ; + if (loading) { + return ( + + + + ); + } return ( - - + + {alertConfig && ( - - + + {alertConfig.msg} )} - - - router.back()}> + + + router.back()} + > - O Meu Perfil + + Ficha de Aluno + isEditing ? salvarPerfil() : setIsEditing(true)} + style={[styles.roundBtn, { backgroundColor: isEditing ? cores.azul : cores.card }]} + onPress={() => isEditing ? salvarDados() : setIsEditing(true)} + disabled={saving} > - + {saving ? ( + + ) : ( + + )} - - - - - - {perfil?.nome?.charAt(0).toUpperCase()} + + {/* HEADER PERFIL */} + + + + {perfil?.nome?.charAt(0).toUpperCase()} - {perfil?.nome} - {perfil?.curso || 'Sem Curso'} • {perfil?.n_escola || '---'} - - - {/* DADOS ACADÉMICOS */} - Informação Académica - - - - setPerfil({...perfil, n_escola: v})} - cores={cores} keyboardType="numeric" - /> - - - setPerfil({...perfil, curso: v})} - cores={cores} autoCapitalize="characters" - /> - + {perfil?.nome} + + + {perfil?.tipo?.toUpperCase()} + - - {/* DADOS PESSOAIS */} - Dados Pessoais - - setPerfil({...perfil, nome: v})} cores={cores} /> + {/* SECÇÃO ACADÉMICA (TABLE ALUNOS) */} + Registo Escolar + + + + + + + + + + + + + {/* SECÇÃO PESSOAL (TABLE PROFILES) */} + Dados de Contacto + + - - - setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} - cores={cores} maxLength={10} keyboardType="numeric" - /> - - - setPerfil({...perfil, telefone: v})} keyboardType="phone-pad" cores={cores} /> - + + + setPerfil({...perfil, idade: v})} cores={cores} keyboardType="numeric" /> + + + setPerfil({...perfil, telefone: v})} cores={cores} keyboardType="phone-pad" maxLength={9} /> + - setPerfil({...perfil, residencia: v})} cores={cores} /> + setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} + cores={cores} + placeholder="DD-MM-AAAA" + /> + + setPerfil({...perfil, residencia: v})} + cores={cores} + /> - {/* ACÇÕES */} - - router.push('/Aluno/redefenirsenha')}> - - + {/* SECÇÃO ESTÁGIO (CORREÇÃO DE LAYOUT) */} + {estagio && ( + <> + Informação de Estágio + + + + + + + + + + + + + {estagio.empresas?.tutor_nome && ( + + )} - Alterar Palavra-passe + + )} + + {/* BOTÕES DE ACÇÃO */} + + router.push('/Aluno/redefenirsenha')} + > + + + + Alterar Credenciais - - + supabase.auth.signOut().then(() => router.replace('/'))} + > + - Terminar Sessão + Terminar Sessão + + {isEditing && ( + { setIsEditing(false); buscarDados(); }}> + Cancelar edições pendentes + + )} - - {isEditing && ( - { setIsEditing(false); carregarDados(); }}> - Cancelar Edição - - )} - - + - + ); } -const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( - +// --- COMPONENTE DE INPUT CUSTOMIZADO --- +const PerfilInput = ({ label, icon, cores, editable, multiline, ...props }: any) => ( + {label} - - - + + + ); const styles = StyleSheet.create({ - safe: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 15, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 10 }, - alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 }, - topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, - backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 }, - topTitle: { fontSize: 18, fontWeight: '800' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, - profileHeader: { alignItems: 'center', marginVertical: 15 }, - avatarContainer: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' }, - avatar: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center', elevation: 4 }, - avatarLetter: { color: '#fff', fontSize: 32, fontWeight: '800' }, - userName: { fontSize: 22, fontWeight: '900', marginTop: 12 }, - userRole: { fontSize: 14, fontWeight: '600' }, - sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2, marginLeft: 10, marginBottom: 10, marginTop: 10 }, - card: { borderRadius: 24, padding: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, - inputWrapper: { marginBottom: 15 }, - inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 }, - inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, height: 50, borderRadius: 16, borderWidth: 1.5 }, - textInput: { flex: 1, fontSize: 14, fontWeight: '600' }, - row: { flexDirection: 'row' }, - actionsContainer: { gap: 10 }, - menuItem: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 18, elevation: 1 }, - menuIcon: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - menuText: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, - cancelBtn: { marginTop: 20, alignItems: 'center' }, - cancelText: { fontSize: 13, fontWeight: '600', textDecorationLine: 'underline' } + headerContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + headerTitle: { fontSize: 19, fontWeight: '900' }, + roundBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowOpacity: 0.1, shadowRadius: 4 }, + scrollContainer: { paddingHorizontal: 20, paddingBottom: 50 }, + avatarSection: { alignItems: 'center', marginVertical: 25 }, + avatarFrame: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' }, + avatarCircle: { width: 95, height: 95, borderRadius: 48, alignItems: 'center', justifyContent: 'center', elevation: 5 }, + avatarInitial: { color: '#fff', fontSize: 40, fontWeight: '800' }, + profileName: { fontSize: 24, fontWeight: '900', marginTop: 15 }, + badge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 }, + badgeText: { fontSize: 11, fontWeight: '800', letterSpacing: 1 }, + sectionHeader: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginLeft: 8, marginBottom: 12, marginTop: 15 }, + infoCard: { borderRadius: 28, padding: 22, elevation: 3, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 10 }, + inputGroup: { marginBottom: 18 }, + inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 16, borderWidth: 1.5 }, + textInput: { flex: 1, fontSize: 15, fontWeight: '700' }, + inputRow: { flexDirection: 'row', justifyContent: 'space-between' }, + footer: { marginTop: 20 }, + actionMenuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, elevation: 2 }, + actionIcon: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + actionText: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '800' }, + cancelLink: { marginTop: 25, alignItems: 'center' }, + cancelLinkText: { fontSize: 14, fontWeight: '700', textDecorationLine: 'underline' }, + alert: { position: 'absolute', left: 20, right: 20, padding: 18, borderRadius: 20, flexDirection: 'row', alignItems: 'center', zIndex: 999, elevation: 10 }, + alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1 } }); \ No newline at end of file