diff --git a/app.json b/app.json index ed4b127..4f74539 100644 --- a/app.json +++ b/app.json @@ -38,6 +38,13 @@ "backgroundColor": "#000000" } } + ], + [ + "expo-image-picker", + { + "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." + } ] ], "experiments": { diff --git a/app/Aluno/perfil.tsx b/app/Aluno/perfil.tsx index 93627e1..9bf0bc8 100644 --- a/app/Aluno/perfil.tsx +++ b/app/Aluno/perfil.tsx @@ -5,9 +5,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Animated, - Dimensions, KeyboardAvoidingView, Platform, + ScrollView, StatusBar, StyleSheet, Text, @@ -19,8 +19,6 @@ 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(); @@ -63,7 +61,6 @@ export default function PerfilAluno() { ]).start(() => setAlertConfig(null)); }, []); - // --- LÓGICA DE FORMATAÇÃO --- const aplicarMascaraData = (text: string) => { const cleaned = text.replace(/\D/g, ''); let formatted = cleaned; @@ -72,46 +69,38 @@ export default function PerfilAluno() { return formatted; }; - // --- CARREGAMENTO DE DADOS --- + // --- CARREGAMENTO DE DADOS COM JOIN DE HORÁRIOS --- const buscarDados = async () => { try { setLoading(true); const { data: { user } } = await supabase.auth.getUser(); if (!user) return; - // Consulta à Tabela Profiles - const { data: pData, error: pError } = await supabase - .from('profiles') - .select('*') - .eq('id', user.id) - .single(); + const { data: pData } = await supabase.from('profiles').select('*').eq('id', user.id).single(); - if (pError) throw pError; - - // 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 (pData?.n_escola) { + const { data: alunoRes } = await supabase.from('alunos').select('*').eq('n_escola', pData.n_escola).single(); if (alunoRes) aData = alunoRes; } - // Consulta à Tabela Estágios + // Join com a tabela horarios_estagio const { data: eData } = await supabase .from('estagios') - .select('*, empresas(nome, tutor_nome)') + .select(` + *, + empresas(nome, tutor_nome), + horarios_estagio(periodo, hora_inicio, hora_fim) + `) .eq('aluno_id', user.id) - .single(); + .maybeSingle(); 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'); + showAlert('Erro ao sincronizar dados.', 'error'); } finally { setLoading(false); } @@ -131,38 +120,24 @@ export default function PerfilAluno() { }).eq('id', perfil.id); if (error) throw error; - setIsEditing(false); - showAlert('Perfil atualizado!', 'success'); + showAlert('Perfil guardado!', 'success'); buscarDados(); } catch (e) { - showAlert('Falha ao guardar alterações.', 'error'); + showAlert('Falha ao guardar.', 'error'); } finally { setSaving(false); } }; - if (loading) { - return ( - - - - ); - } + if (loading) return ; return ( - + {alertConfig && ( - + {alertConfig.msg} @@ -170,168 +145,109 @@ export default function PerfilAluno() { - router.back()} - > + router.back()}> - Ficha de Aluno - isEditing ? salvarDados() : setIsEditing(true)} disabled={saving} > - {saving ? ( - - ) : ( - - )} + {saving ? : } - - {/* HEADER PERFIL */} - - - - {perfil?.nome?.charAt(0).toUpperCase()} - - - {perfil?.nome} - - - {perfil?.tipo?.toUpperCase()} - - - - - {/* SECÇÃO ACADÉMICA (TABLE ALUNOS) */} - Registo Escolar - - - - - - - - - - - - - {/* SECÇÃO PESSOAL (TABLE PROFILES) */} - Dados de Contacto - - + + - - - setPerfil({...perfil, idade: v})} cores={cores} keyboardType="numeric" /> - - - setPerfil({...perfil, telefone: v})} cores={cores} keyboardType="phone-pad" maxLength={9} /> - - - - setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} - cores={cores} - placeholder="DD-MM-AAAA" - /> - - setPerfil({...perfil, residencia: v})} - cores={cores} - /> - - - {/* SECÇÃO ESTÁGIO (CORREÇÃO DE LAYOUT) */} - {estagio && ( - <> - Informação de Estágio - - - - - - - - - - + {/* CABEÇALHO */} + + + + {perfil?.nome?.charAt(0).toUpperCase()} - - {estagio.empresas?.tutor_nome && ( - - )} - - )} - - {/* BOTÕES DE ACÇÃO */} - - router.push('/Aluno/redefenirsenha')} - > - - + {perfil?.nome} + + {perfil?.tipo?.toUpperCase()} - Alterar Credenciais - - + - supabase.auth.signOut().then(() => router.replace('/'))} - > - - + {/* DADOS ESCOLARES */} + Registo Escolar + + + + - Terminar Sessão - + + - {isEditing && ( - { setIsEditing(false); buscarDados(); }}> - Cancelar edições pendentes - + {/* DADOS PESSOAIS */} + Contactos e Pessoal + + + + setPerfil({...perfil, idade: v})} cores={cores} keyboardType="numeric" /> + setPerfil({...perfil, telefone: v})} cores={cores} keyboardType="phone-pad" maxLength={9} /> + + setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} cores={cores} placeholder="DD-MM-AAAA" /> + + + {/* SECÇÃO ESTÁGIO COM LOGICA DE HORARIOS RELACIONADOS */} + {estagio && ( + <> + Informação de Estágio + + + + + + + + + 0 + ? estagio.horarios_estagio.map((h: any) => + `${h.periodo}: ${h.hora_inicio.slice(0,5)}-${h.hora_fim.slice(0,5)}` + ).join('\n') + : "Não definido" + } + editable={false} + cores={cores} + multiline={true} + /> + + + + {estagio.empresas?.tutor_nome && } + + )} - - + + {/* ACÇÕES */} + + router.push('/Aluno/redefenirsenha')}> + + Redefinir Senha + + + supabase.auth.signOut().then(() => router.replace('/'))}> + + Terminar Sessão + + + + ); } -// --- COMPONENTE DE INPUT CUSTOMIZADO --- const PerfilInput = ({ label, icon, cores, editable, multiline, ...props }: any) => ( {label} @@ -339,15 +255,16 @@ const PerfilInput = ({ label, icon, cores, editable, multiline, ...props }: any) backgroundColor: cores.fundo, borderColor: editable ? cores.azul : cores.borda, height: multiline ? undefined : 52, - minHeight: multiline ? 52 : undefined, - paddingVertical: multiline ? 10 : 0 + minHeight: 52, + paddingVertical: multiline ? 8 : 0 }]}> - + @@ -358,8 +275,8 @@ const styles = StyleSheet.create({ centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, 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 }, + roundBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 2 }, + scrollContainer: { paddingHorizontal: 20, paddingBottom: 20 }, 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 }, @@ -367,19 +284,17 @@ const styles = StyleSheet.create({ 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 }, + sectionHeader: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginLeft: 8, marginBottom: 12, marginTop: 15 }, + infoCard: { borderRadius: 25, padding: 20, elevation: 3, shadowOpacity: 0.1 }, + inputGroup: { marginBottom: 15 }, + inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 }, inputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 16, borderWidth: 1.5 }, - textInput: { flex: 1, fontSize: 15, fontWeight: '700' }, + textInput: { flex: 1, fontSize: 14, 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 }, + alert: { position: 'absolute', left: 20, right: 20, padding: 18, borderRadius: 20, flexDirection: 'row', alignItems: 'center', zIndex: 999 }, alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1 } }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3331cbb..bd47bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "expo-font": "~14.0.10", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", + "expo-image-picker": "~17.0.10", "expo-linear-gradient": "~15.0.8", "expo-linking": "~8.0.10", "expo-location": "~19.0.8", @@ -6573,6 +6574,27 @@ } } }, + "node_modules/expo-image-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz", + "integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz", + "integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~6.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-keep-awake": { "version": "15.0.8", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", diff --git a/package.json b/package.json index cb9ce8e..cce23dd 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "expo-font": "~14.0.10", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", + "expo-image-picker": "~17.0.10", "expo-linear-gradient": "~15.0.8", "expo-linking": "~8.0.10", "expo-location": "~19.0.8",