diff --git a/app.json b/app.json index 4f74539..71b8816 100644 --- a/app.json +++ b/app.json @@ -10,10 +10,10 @@ "newArchEnabled": true, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.teu-nome.estagiospap" + "bundleIdentifier": "com.epvc.estagiospap" }, "android": { - "package": "com.teu_nome.estagiospap", + "package": "com.epvc.estagiospap", "adaptiveIcon": { "backgroundColor": "#E6F4FE", "foregroundImage": "./assets/images/android-icon-foreground.png", diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index a7c9e98..3eeeb2c 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -22,8 +22,8 @@ import { View } from 'react-native'; import { Calendar, LocaleConfig } from 'react-native-calendars'; +import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -import { supabase } from '../lib/supabase'; // Configuração PT-PT para o Calendário LocaleConfig.locales['pt'] = { diff --git a/app/Aluno/definicoes.tsx b/app/Aluno/definicoes.tsx index be17851..e88a21b 100644 --- a/app/Aluno/definicoes.tsx +++ b/app/Aluno/definicoes.tsx @@ -14,8 +14,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -import { supabase } from '../lib/supabase'; const Definicoes = memo(() => { const router = useRouter(); diff --git a/app/Aluno/perfil.tsx b/app/Aluno/perfil.tsx index 9bf0bc8..d369a1c 100644 --- a/app/Aluno/perfil.tsx +++ b/app/Aluno/perfil.tsx @@ -1,9 +1,9 @@ -// app/Aluno/PerfilAluno.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, + Alert, Animated, KeyboardAvoidingView, Platform, @@ -16,60 +16,102 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -import { supabase } from '../lib/supabase'; 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 fadeAnim = useMemo(() => new Animated.Value(0), []); + const [faltasJustificadas, setFaltasJustificadas] = useState(0); + const [faltasInjustificadas, setFaltasInjustificadas] = useState(0); - const azulPetroleo = '#2390a6'; + const fadeAnim = useMemo(() => new Animated.Value(0), []); const cores = useMemo(() => ({ fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', card: isDarkMode ? '#1A1A1A' : '#FFFFFF', texto: isDarkMode ? '#F8FAFC' : '#1E293B', secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, + azul: '#2390a6', azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)', vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', vermelho: '#EF4444', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', verde: '#10B981', - sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.05)', }), [isDarkMode]); - 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 VERIFICAÇÃO DO ESTÁGIO ATIVO --- + const isEstagioAtivo = useMemo(() => { + if (!estagio) return false; // Se não tem estágio na DB, não está ativo + if (!estagio.data_fim) return true; // Se tem estágio mas sem data de fim, assumimos ativo + + const hoje = new Date(); + hoje.setHours(0, 0, 0, 0); // Reset às horas para comparar só o dia + + const dataFim = new Date(estagio.data_fim); + dataFim.setHours(0, 0, 0, 0); + + return dataFim >= hoje; // Se a data de fim for hoje ou no futuro, está ativo + }, [estagio]); + + // --- FUNÇÕES DE DATA --- + const formatarDataParaUI = (dataDB: string) => { + if (!dataDB) return ''; + const parts = dataDB.split('-'); + if (parts.length !== 3) return dataDB; + return `${parts[2]}-${parts[1]}-${parts[0]}`; + }; + + const formatarDataParaDB = (dataUI: string) => { + if (!dataUI || dataUI.length !== 10) return null; + const parts = dataUI.split('-'); + if (parts.length !== 3) return null; + return `${parts[2]}-${parts[1]}-${parts[0]}`; + }; + + const calcularIdade = (dataPT: string) => { + if (!dataPT || dataPT.length !== 10) return null; + const parts = dataPT.split('-'); + const dia = parseInt(parts[0], 10); + const mes = parseInt(parts[1], 10); + const ano = parseInt(parts[2], 10); + + if (ano < 1950 || ano > new Date().getFullYear()) return null; + + const hoje = new Date(); + const nascimento = new Date(ano, mes - 1, dia); + + if (nascimento.getDate() !== dia) return null; + + let idade = hoje.getFullYear() - nascimento.getFullYear(); + const m = hoje.getMonth() - nascimento.getMonth(); + if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) { + idade--; + } + return idade >= 0 ? idade : 0; + }; const aplicarMascaraData = (text: string) => { const cleaned = text.replace(/\D/g, ''); let formatted = cleaned; - if (cleaned.length > 2) formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`; - if (cleaned.length > 4) formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`; + if (cleaned.length > 2 && cleaned.length <= 4) { + formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`; + } else if (cleaned.length > 4) { + formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`; + } return formatted; }; - // --- CARREGAMENTO DE DADOS COM JOIN DE HORÁRIOS --- + // --- FIM FUNÇÕES DATA --- + const buscarDados = async () => { try { setLoading(true); @@ -84,23 +126,35 @@ export default function PerfilAluno() { if (alunoRes) aData = alunoRes; } - // Join com a tabela horarios_estagio const { data: eData } = await supabase .from('estagios') - .select(` - *, - empresas(nome, tutor_nome), - horarios_estagio(periodo, hora_inicio, hora_fim) - `) + .select(`*, empresas(*), horarios_estagio(*)`) .eq('aluno_id', user.id) .maybeSingle(); - setPerfil({ ...pData, ...aData, email: user.email }); + const { data: presencas } = await supabase.from('presencas').select('estado, justificacao_url').eq('aluno_id', user.id); + + if (presencas) { + setFaltasJustificadas(presencas.filter(p => p.estado === 'Falta' && p.justificacao_url).length); + setFaltasInjustificadas(presencas.filter(p => p.estado === 'Falta' && !p.justificacao_url).length); + } + + const dataFormatadaUI = formatarDataParaUI(pData?.data_nascimento); + const idadeCalculada = dataFormatadaUI ? calcularIdade(dataFormatadaUI) : pData?.idade; + + setPerfil({ + ...aData, + ...pData, + email: user.email, + data_nascimento: dataFormatadaUI, + idade: idadeCalculada ?? 'N/A' + }); + setEstagio(eData); Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start(); } catch (err) { - showAlert('Erro ao sincronizar dados.', 'error'); + console.error(err); } finally { setLoading(false); } @@ -111,20 +165,30 @@ export default function PerfilAluno() { const salvarDados = async () => { try { setSaving(true); - const { error } = await supabase.from('profiles').update({ + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return; + + const dataProntaParaDB = formatarDataParaDB(perfil.data_nascimento); + + const { error, data } = await supabase.from('profiles').update({ nome: perfil.nome, - telefone: perfil.telefone, residencia: perfil.residencia, - idade: perfil.idade, - data_nascimento: perfil.data_nascimento - }).eq('id', perfil.id); + data_nascimento: dataProntaParaDB, + idade: perfil.idade !== 'N/A' ? Number(perfil.idade) : null, + telefone: perfil.telefone + }).eq('id', user.id).select(); if (error) throw error; + + if (!data || data.length === 0) { + throw new Error("Erro de RLS. Confirma as tuas políticas no Supabase."); + } + setIsEditing(false); - showAlert('Perfil guardado!', 'success'); - buscarDados(); - } catch (e) { - showAlert('Falha ao guardar.', 'error'); + Alert.alert("Sucesso", "Perfil atualizado!"); + await buscarDados(); + } catch (e: any) { + Alert.alert("Erro ao gravar", e.message); } finally { setSaving(false); } @@ -135,15 +199,8 @@ export default function PerfilAluno() { return ( - - {alertConfig && ( - - - {alertConfig.msg} - - )} - + router.back()}> @@ -161,86 +218,115 @@ export default function PerfilAluno() { - {/* CABEÇALHO */} - - - {perfil?.nome?.charAt(0).toUpperCase()} - + + {perfil?.nome?.charAt(0).toUpperCase()} - {perfil?.nome} + setPerfil({...perfil, nome: v})} + placeholder="Nome do Aluno" + placeholderTextColor={cores.secundario} + /> - {perfil?.tipo?.toUpperCase()} + {perfil?.turma_curso || 'CURSO NÃO DEFINIDO'} - {/* DADOS ESCOLARES */} Registo Escolar - + - - - - - - - {/* 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 && } + + - + + + + + + + + Dados Pessoais + + { + const masked = aplicarMascaraData(v); + const novaIdade = calcularIdade(masked); + setPerfil({...perfil, data_nascimento: masked, idade: novaIdade ?? 'N/A'}); + }} + cores={cores} + placeholder="DD-MM-AAAA" + maxLength={10} + /> + + + + + + setPerfil({...perfil, telefone: v})} cores={cores} keyboardType="phone-pad" /> + + + setPerfil({...perfil, residencia: v})} cores={cores} /> + + + Informação de Estágio + + {/* Aqui entra a condição: Só mostra a info se isEstagioAtivo for verdadeiro */} + {isEstagioAtivo ? ( + + + + + + + + + + + + + JUSTIFICADAS: {faltasJustificadas} + + + INJUSTIFICADAS: {faltasInjustificadas} + + + + ) : ( + + + {/* Mensagem dinâmica se já teve ou se nunca teve estágio */} + + {estagio ? "Sem estágio ativo no momento." : "Sem estágio atribuído no sistema"} + + )} - {/* ACÇÕES */} - router.push('/Aluno/redefenirsenha')}> - - Redefinir Senha - + router.push('/redefenirsenha')}> + + + + Alterar palavra-passe + - supabase.auth.signOut().then(() => router.replace('/'))}> - + + supabase.auth.signOut().then(() => router.replace('/'))} + > + + + Terminar Sessão + @@ -248,23 +334,15 @@ export default function PerfilAluno() { ); } -const PerfilInput = ({ label, icon, cores, editable, multiline, ...props }: any) => ( +const PerfilInput = ({ label, icon, cores, editable, ...props }: any) => ( {label} - - + + @@ -275,26 +353,25 @@ 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 }, - 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 }, + roundBtn: { width: 45, height: 45, borderRadius: 14, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 }, + scrollContainer: { paddingHorizontal: 20 }, + avatarSection: { alignItems: 'center', marginVertical: 20 }, + avatarCircle: { width: 100, height: 100, borderRadius: 50, alignItems: 'center', justifyContent: 'center', elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, 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: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginLeft: 8, marginBottom: 12, marginTop: 15 }, - infoCard: { borderRadius: 25, padding: 20, elevation: 3, shadowOpacity: 0.1 }, + profileName: { fontSize: 24, fontWeight: '900', marginTop: 15, textAlign: 'center', width: '100%' }, + badge: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 10, marginTop: 8 }, + badgeText: { fontSize: 10, fontWeight: '900', letterSpacing: 1, textTransform: 'uppercase' }, + sectionHeader: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12, marginTop: 25, marginLeft: 5 }, + infoCard: { borderRadius: 24, padding: 20, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 10 }, 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: 14, fontWeight: '700' }, + inputLabel: { fontSize: 9, fontWeight: '900', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4, letterSpacing: 0.5 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 14, borderWidth: 1.5, height: 52 }, + 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' }, - 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 } + miniStatus: { padding: 12, borderRadius: 14, alignItems: 'center', justifyContent: 'center' }, + miniStatusText: { fontSize: 10, fontWeight: '900' }, + footer: { marginTop: 25 }, + actionMenuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 3 }, + actionIcon: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + actionText: { flex: 1, marginLeft: 15, fontSize: 15, fontWeight: '800' } }); \ No newline at end of file diff --git a/app/Professor/Alunos/CalendarioPresencas.tsx b/app/Professor/Alunos/CalendarioPresencas.tsx index 3005c00..2994fd8 100644 --- a/app/Professor/Alunos/CalendarioPresencas.tsx +++ b/app/Professor/Alunos/CalendarioPresencas.tsx @@ -16,8 +16,8 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- INTERFACES --- interface Presenca { diff --git a/app/Professor/Alunos/CriarAluno.tsx b/app/Professor/Alunos/CriarAluno.tsx index 44fd7b8..f0e8e38 100644 --- a/app/Professor/Alunos/CriarAluno.tsx +++ b/app/Professor/Alunos/CriarAluno.tsx @@ -15,8 +15,8 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // Função para calcular idade automaticamente const calcularIdade = (data: string): string => { diff --git a/app/Professor/Alunos/DetalhesAluno.tsx b/app/Professor/Alunos/DetalhesAluno.tsx index d5e1ac4..2bf95a9 100644 --- a/app/Professor/Alunos/DetalhesAluno.tsx +++ b/app/Professor/Alunos/DetalhesAluno.tsx @@ -16,8 +16,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- UTILITÁRIOS --- const calcularIdade = (dataNascimento: string) => { diff --git a/app/Professor/Alunos/Estagios.tsx b/app/Professor/Alunos/Estagios.tsx index 867fd69..1df7d01 100644 --- a/app/Professor/Alunos/Estagios.tsx +++ b/app/Professor/Alunos/Estagios.tsx @@ -15,8 +15,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- Interfaces --- interface Aluno { id: string; nome: string; turma_curso: string; ano: number; } diff --git a/app/Professor/Alunos/Faltas.tsx b/app/Professor/Alunos/Faltas.tsx index 3be8e41..835e925 100644 --- a/app/Professor/Alunos/Faltas.tsx +++ b/app/Professor/Alunos/Faltas.tsx @@ -18,8 +18,8 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- INTERFACES --- export interface Aluno { diff --git a/app/Professor/Alunos/ListaAlunos.tsx b/app/Professor/Alunos/ListaAlunos.tsx index 6565cc2..cddd442 100644 --- a/app/Professor/Alunos/ListaAlunos.tsx +++ b/app/Professor/Alunos/ListaAlunos.tsx @@ -16,8 +16,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- INTERFACES --- export interface Aluno { diff --git a/app/Professor/Alunos/Presencas.tsx b/app/Professor/Alunos/Presencas.tsx index b6f131f..8cd9126 100644 --- a/app/Professor/Alunos/Presencas.tsx +++ b/app/Professor/Alunos/Presencas.tsx @@ -14,8 +14,8 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- INTERFACES --- export interface Aluno { diff --git a/app/Professor/Alunos/Sumarios.tsx b/app/Professor/Alunos/Sumarios.tsx index bcd3f5d..cb08b1f 100644 --- a/app/Professor/Alunos/Sumarios.tsx +++ b/app/Professor/Alunos/Sumarios.tsx @@ -16,8 +16,8 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; // --- INTERFACES --- export interface Aluno { diff --git a/app/Professor/Empresas/DetalhesEmpresa.tsx b/app/Professor/Empresas/DetalhesEmpresa.tsx index 2ff65fc..cf29117 100644 --- a/app/Professor/Empresas/DetalhesEmpresa.tsx +++ b/app/Professor/Empresas/DetalhesEmpresa.tsx @@ -16,8 +16,8 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; export interface Empresa { id: string; diff --git a/app/Professor/Empresas/ListaEmpresas.tsx b/app/Professor/Empresas/ListaEmpresas.tsx index 963c84c..ef1ba30 100644 --- a/app/Professor/Empresas/ListaEmpresas.tsx +++ b/app/Professor/Empresas/ListaEmpresas.tsx @@ -19,8 +19,8 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -import { supabase } from '../../lib/supabase'; export interface Empresa { id: number; diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index a4b8d3c..e7ce7da 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -14,8 +14,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -import { supabase } from '../lib/supabase'; interface PerfilData { id: string; diff --git a/app/Professor/ProfessorHome.tsx b/app/Professor/ProfessorHome.tsx index 2d4b925..412550b 100644 --- a/app/Professor/ProfessorHome.tsx +++ b/app/Professor/ProfessorHome.tsx @@ -13,8 +13,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -import { supabase } from '../lib/supabase'; const { width } = Dimensions.get('window'); diff --git a/app/Professor/defenicoes2.tsx b/app/Professor/defenicoes2.tsx index 5b513c0..ea42b86 100644 --- a/app/Professor/defenicoes2.tsx +++ b/app/Professor/defenicoes2.tsx @@ -14,8 +14,8 @@ import { View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; -import { supabase } from '../lib/supabase'; const Definicoes = memo(() => { const router = useRouter(); diff --git a/app/Professor/redefenirsenha2.tsx b/app/Professor/redefenirsenha2.tsx index 9b086eb..f43e670 100644 --- a/app/Professor/redefenirsenha2.tsx +++ b/app/Professor/redefenirsenha2.tsx @@ -14,7 +14,7 @@ import { TouchableOpacity, View, } from 'react-native'; -import { supabase } from '../../app/lib/supabase'; +import { supabase } from '../../lib/supabase'; export default function ForgotPassword() { const [email, setEmail] = useState(''); diff --git a/app/_layout.tsx b/app/_layout.tsx index 00c0a10..70e02e8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,17 +1,47 @@ // app/_layout.tsx -import { NavigationContainer } from '@react-navigation/native'; -import { Stack } from 'expo-router'; +import { Stack, useRouter } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; +import { useEffect } from 'react'; +// CORREÇÃO: O caminho mudou porque moveste a pasta lib para a raiz +import { supabase } from '../lib/supabase'; import { ThemeProvider, useTheme } from '../themecontext'; function RootLayoutContent() { const { isDarkMode } = useTheme(); + const router = useRouter(); + + useEffect(() => { + // Escuta mudanças no estado de autenticação + const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => { + console.log("EVENTO SUPABASE:", event); + + if (event === 'PASSWORD_RECOVERY') { + console.log("A redirecionar para: /novapasse"); + + // Usamos um delay ligeiramente maior para garantir que o RootLayout + // já montou a pilha de navegação (Stack) + const timer = setTimeout(() => { + // Usamos replace para não permitir que o user volte para uma rota "quebrada" + router.replace('/novapasse'); + }, 1000); + + return () => clearTimeout(timer); + } + }); + + return () => { + authListener.subscription.unsubscribe(); + }; + }, []); + return ( <> - {/* Removido o .tsx do name, o Expo Router usa apenas o nome do ficheiro */} - + {/* O index é o teu login */} + + {/* Garantimos que a rota novapasse existe na stack */} + ); @@ -23,10 +53,4 @@ export default function RootLayout() { ); -} - - - - {/* aluno e professor */} - - +} \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 863fac1..9828b69 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -11,7 +11,7 @@ import { View } from 'react-native'; import Auth from '../components/Auth'; -import { supabase } from './lib/supabase'; +import { supabase } from '../lib/supabase'; export default function LoginScreen() { const router = useRouter(); diff --git a/app/novapasse.tsx b/app/novapasse.tsx new file mode 100644 index 0000000..f200dc5 --- /dev/null +++ b/app/novapasse.tsx @@ -0,0 +1,239 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; +import { useState } from 'react'; +import { + ActivityIndicator, + KeyboardAvoidingView, + Platform, + ScrollView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +// CORREÇÃO 1: O caminho agora é ../lib/supabase porque moveste a pasta para a raiz +import { supabase } from '../lib/supabase'; + +// CORREÇÃO 2: O nome da função deve ser preferencialmente o nome do ficheiro +export default function NovaPasse() { + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const [status, setStatus] = useState<{ type: 'error' | 'success'; msg: string } | null>(null); + + const router = useRouter(); + + const handleUpdatePassword = async () => { + setStatus(null); + + if (password.length < 6) { + setStatus({ type: 'error', msg: 'A password deve ter pelo menos 6 caracteres.' }); + return; + } + + if (password !== confirmPassword) { + setStatus({ type: 'error', msg: 'As passwords não coincidem.' }); + return; + } + + setLoading(true); + try { + // O utilizador já está autenticado pelo link de recuperação + const { error } = await supabase.auth.updateUser({ password: password }); + + if (error) throw error; + + setStatus({ type: 'success', msg: 'Sucesso! A tua password foi atualizada.' }); + + // Manda para o ecrã inicial (index) após 3 segundos + setTimeout(() => router.replace('/'), 3000); + + } catch (err: any) { + setStatus({ type: 'error', msg: 'Erro ao atualizar. O link pode ter expirado.' }); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + + + + + + + + + Nova Palavra-passe + + Cria uma nova senha segura para acederes à tua conta. + + + + {status && ( + + + + {status.msg} + + + )} + + + + Nova Password + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + secureTextEntry + editable={!loading} + /> + + + + Confirmar Password + + + + + {loading ? ( + + ) : ( + Atualizar Password + )} + + + + + + Segurança EPVC Estágios+ • 2026 + + + + + ); +} + +const styles = StyleSheet.create({ + mainContainer: { flex: 1, backgroundColor: '#FFFFFF' }, + scrollContainer: { + flexGrow: 1, + paddingHorizontal: 28, + paddingTop: 80, + paddingBottom: 40, + }, + content: { flex: 1, justifyContent: 'center' }, + header: { alignItems: 'center', marginBottom: 35 }, + iconWrapper: { position: 'relative', marginBottom: 20 }, + iconCircle: { + width: 90, + height: 90, + backgroundColor: '#F0F7FF', + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + borderColor: '#DBEAFE', + }, + iconBadge: { + position: 'absolute', + bottom: -4, + right: -4, + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: '#2390a6', + borderWidth: 3, + borderColor: '#FFFFFF', + }, + title: { + fontSize: 28, + fontWeight: '900', + color: '#0F172A', + textAlign: 'center', + letterSpacing: -1, + }, + subtitle: { + fontSize: 15, + color: '#64748B', + textAlign: 'center', + marginTop: 10, + lineHeight: 22, + maxWidth: 300, + }, + statusBanner: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderRadius: 16, + marginBottom: 25, + borderWidth: 1, + }, + statusText: { fontSize: 14, fontWeight: '600', marginLeft: 10, flex: 1 }, + errorBg: { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' }, + errorText: { color: '#B91C1C' }, + successBg: { backgroundColor: '#F0FDF4', borderColor: '#DCFCE7' }, + successText: { color: '#166534' }, + form: { width: '100%' }, + inputWrapper: { marginBottom: 24 }, + label: { fontSize: 14, fontWeight: '700', color: '#475569', marginBottom: 10, marginLeft: 4 }, + input: { + backgroundColor: '#FBFDFF', + borderRadius: 18, + paddingHorizontal: 20, + paddingVertical: 18, + fontSize: 16, + color: '#0F172A', + borderWidth: 1.5, + borderColor: '#F1F5F9', + }, + inputFocused: { borderColor: '#2390a6', backgroundColor: '#FFFFFF' }, + button: { + backgroundColor: '#dd8707', + borderRadius: 18, + paddingVertical: 20, + alignItems: 'center', + elevation: 6, + }, + buttonDisabled: { backgroundColor: '#E2E8F0', elevation: 0 }, + buttonText: { color: '#fff', fontSize: 16, fontWeight: '800' }, + footer: { marginTop: 40, alignItems: 'center' }, + footerText: { fontSize: 12, color: '#CBD5E1', fontWeight: '600' }, +}); \ No newline at end of file diff --git a/app/redefenirsenha.tsx b/app/redefenirsenha.tsx index 4494959..a8661a6 100644 --- a/app/redefenirsenha.tsx +++ b/app/redefenirsenha.tsx @@ -14,7 +14,7 @@ import { TouchableOpacity, View, } from 'react-native'; -import { supabase } from '../app/lib/supabase'; +import { supabase } from '../lib/supabase'; export default function ForgotPassword() { const [email, setEmail] = useState(''); diff --git a/components/Auth.tsx b/components/Auth.tsx index a7d2341..ef98f24 100644 --- a/components/Auth.tsx +++ b/components/Auth.tsx @@ -10,7 +10,7 @@ import { TouchableOpacity, View } from 'react-native'; -import { supabase } from '../app/lib/supabase'; +import { supabase } from '../lib/supabase'; interface AuthProps { onLoginSuccess?: () => void; diff --git a/app/lib/supabase.ts b/lib/supabase.ts similarity index 100% rename from app/lib/supabase.ts rename to lib/supabase.ts