From bba6d2de08b7076d8b71eb746db13be50b7976a1 Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Tue, 21 Apr 2026 17:19:35 +0100 Subject: [PATCH] criar/dados aluno --- app/Aluno/AlunoHome.tsx | 93 ++++++-- app/Aluno/perfil.tsx | 179 +++++--------- app/Professor/Alunos/CriarAluno.tsx | 242 +++++++++++++------ app/Professor/Alunos/DetalhesAluno.tsx | 314 +++++++++++++++++-------- app/Professor/Alunos/ListaAlunos.tsx | 76 +++--- app/Professor/PerfilProf.tsx | 78 +++--- 6 files changed, 596 insertions(+), 386 deletions(-) diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index bd38df8..a7c9e98 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -76,6 +76,8 @@ const AlunoHome = memo(() => { azul: azulPetroleo, vermelho: '#EF4444', verde: '#10B981', + aviso: isDarkMode ? '#2D2200' : '#FFF9E6', + avisoTexto: isDarkMode ? '#FFD700' : '#856404' }), [isDarkMode]); const showAlert = (msg: string, type: 'success' | 'error' | 'info' = 'info') => { @@ -95,7 +97,13 @@ const AlunoHome = memo(() => { const { data: { user } } = await supabase.auth.getUser(); if (!user) return; const { data: estagio } = await supabase.from('estagios').select('data_inicio, data_fim').eq('aluno_id', user.id).single(); - if (estagio) setConfigEstagio({ inicio: estagio.data_inicio, fim: estagio.data_fim }); + + if (estagio) { + setConfigEstagio({ inicio: estagio.data_inicio, fim: estagio.data_fim }); + } else { + setConfigEstagio({ inicio: '', fim: '' }); + } + const { data, error } = await supabase.from('presencas').select('*').eq('aluno_id', user.id); if (error) throw error; const p: any = {}, f: any = {}, s: any = {}, u: any = {}; @@ -104,26 +112,41 @@ const AlunoHome = memo(() => { else { f[item.data] = true; u[item.data] = item.justificacao_url || ''; } }); setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u); - } catch (error) { console.error(error); } - finally { setIsLoadingDB(false); } + } catch (error) { + console.error(error); + } finally { + setIsLoadingDB(false); + } }; const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]); + // LOGICA CORRIGIDA: Se as datas forem '', o estágio não existe e bloqueia tudo const infoData = useMemo(() => { const data = new Date(selectedDate); const diaSemana = data.getDay(); const nomeFeriado = feriadosMap[selectedDate]; - const antesDoInicio = configEstagio.inicio && selectedDate < configEstagio.inicio; - const depoisDoFim = configEstagio.fim && selectedDate > configEstagio.fim; + + const temEstagio = configEstagio.inicio !== '' && configEstagio.fim !== ''; + const antesDoInicio = temEstagio && selectedDate < configEstagio.inicio; + const depoisDoFim = temEstagio && selectedDate > configEstagio.fim; + return { - valida: diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado, - podeMarcar: selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado, - nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: antesDoInicio || depoisDoFim + valida: temEstagio && diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado, + podeMarcar: temEstagio && selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado, + nomeFeriado, + antesDoInicio, + depoisDoFim, + foraDeRange: !temEstagio || antesDoInicio || depoisDoFim, + temEstagio }; }, [selectedDate, configEstagio, hojeStr, feriadosMap]); const handlePresencaClick = async () => { + if (!infoData.temEstagio) { + showAlert("Aguarde pela configuração do estágio.", "error"); + return; + } const { status } = await Location.getForegroundPermissionsAsync(); if (status === 'granted') { executarMarcacao(); @@ -143,15 +166,15 @@ const AlunoHome = memo(() => { await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude }); - showAlert("Presença marcada com sucesso!", "success"); + showAlert("Presença marcada!", "success"); fetchDadosSupabase(); } catch (e: any) { showAlert(e.message, "error"); } finally { setIsLocating(false); } }; const handleFalta = async () => { - if (infoData.foraDeRange) return showAlert("Fora do período de estágio.", "error"); - if (!infoData.valida) return showAlert("Não podes marcar falta neste dia.", "error"); + if (infoData.foraDeRange) return showAlert("Data fora do período de estágio.", "error"); + if (!infoData.valida) return showAlert("Não é possível registar falta hoje.", "error"); try { const { data: { user } } = await supabase.auth.getUser(); await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' }); @@ -177,7 +200,7 @@ const AlunoHome = memo(() => { const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName); await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate }); setPdf(null); - showAlert("Justificativo enviado!", "success"); + showAlert("Enviado com sucesso!", "success"); fetchDadosSupabase(); } catch (e) { showAlert("Erro no upload.", "error"); } finally { setIsUploading(false); } @@ -188,9 +211,9 @@ const AlunoHome = memo(() => { const { data: { user } } = await supabase.auth.getUser(); await supabase.from('presencas').update({ sumario: sumarios[selectedDate] }).match({ aluno_id: user?.id, data: selectedDate }); setEditandoSumario(false); - showAlert("Sumário atualizado!", "success"); + showAlert("Sumário guardado!", "success"); fetchDadosSupabase(); - } catch (e) { showAlert("Erro ao guardar sumário.", "error"); } + } catch (e) { showAlert("Erro ao guardar.", "error"); } }; return ( @@ -204,15 +227,15 @@ const AlunoHome = memo(() => { - Validar Localização + Confirmar Local - Para registar a tua presença, precisamos de confirmar que te encontras no local de estágio. + Precisamos de validar a tua localização para confirmar que estás no estágio. Confirmar e Marcar setShowLocationModal(false)}> - Agora não + Cancelar @@ -237,16 +260,34 @@ const AlunoHome = memo(() => { + {/* AVISO DE FALTA DE ESTÁGIO - Vai dar merda se o aluno não souber por que está bloqueado */} + {!infoData.temEstagio && !isLoadingDB && ( + + + + O teu período de estágio ainda não foi configurado pelo professor. + + + )} + {isLocating ? : Marcar Presença} @@ -280,7 +321,7 @@ const AlunoHome = memo(() => { {presencas[selectedDate] && ( - Sumário do Dia + Sumário setEditandoSumario(true)}> { multiline editable={editandoSumario} value={sumarios[selectedDate]} onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})} - placeholder="Descreve o que fizeste..." + placeholder="O que fizeste hoje?" placeholderTextColor="#94A3B8" /> {editandoSumario && Guardar Sumário} @@ -297,11 +338,11 @@ const AlunoHome = memo(() => { {faltas[selectedDate] && ( - Justificar Falta + Justificar {urlsJustificacao[selectedDate] ? ( - Documento Enviado + Justificativo Enviado ) : ( <> @@ -311,7 +352,7 @@ const AlunoHome = memo(() => { {pdf && ( - {isUploading ? : Submeter Justificativo} + {isUploading ? : Submeter} )} @@ -331,10 +372,12 @@ const styles = StyleSheet.create({ title: { fontSize: 26, fontWeight: '900' }, alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 15, zIndex: 1000 }, alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' }, + avisoBox: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 15, borderRadius: 15, marginBottom: 20 }, + avisoTexto: { fontSize: 13, fontWeight: '700', flex: 1 }, botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 }, btn: { padding: 18, borderRadius: 22, width: '48%', alignItems: 'center', elevation: 3 }, txtBtn: { color: '#fff', fontWeight: '800', fontSize: 14 }, - disabled: { opacity: 0.4 }, + disabled: { opacity: 0.3 }, cardCalendar: { borderRadius: 30, padding: 10, borderWidth: 1, overflow: 'hidden' }, card: { padding: 20, borderRadius: 25, marginTop: 20, borderWidth: 1 }, cardTitulo: { fontSize: 18, fontWeight: '700' }, diff --git a/app/Aluno/perfil.tsx b/app/Aluno/perfil.tsx index 2ad34b3..dfcd548 100644 --- a/app/Aluno/perfil.tsx +++ b/app/Aluno/perfil.tsx @@ -19,9 +19,6 @@ export default function PerfilAluno() { const [loading, setLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); const [perfil, setPerfil] = useState(null); - const [estagio, setEstagio] = useState(null); - const [contagemPresencas, setContagemPresencas] = useState(0); - const [contagemFaltas, setContagemFaltas] = useState(0); const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); const alertOpacity = useMemo(() => new Animated.Value(0), []); @@ -48,7 +45,6 @@ export default function PerfilAluno() { vermelho: '#EF4444', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', verde: '#10B981', - laranja: '#dd8707', }), [isDarkMode]); const formatarParaExibir = (data: string) => { @@ -71,59 +67,27 @@ export default function PerfilAluno() { return formatted; }; - const calcularHorasTotaisUteis = (dataInicio: string, dataFim: string) => { - if (!dataInicio || !dataFim) return 400; - let inicio = new Date(dataInicio); - let fim = new Date(dataFim); - let diasUteis = 0; - const feriados = ['2026-01-01', '2026-04-03', '2026-04-25', '2026-05-01', '2026-06-10', '2026-08-15', '2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25']; - - let dataAtual = new Date(inicio); - while (dataAtual <= fim) { - const diaSemana = dataAtual.getDay(); - const dataIso = dataAtual.toISOString().split('T')[0]; - if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) diasUteis++; - dataAtual.setDate(dataAtual.getDate() + 1); - } - return diasUteis * 7; - }; - const carregarDados = async () => { try { setLoading(true); const { data: { user } } = await supabase.auth.getUser(); if (!user) return; - const [perfilRes, estagioRes, presencasRes] = await Promise.all([ - supabase.from('profiles').select('*').eq('id', user.id).single(), - supabase.from('estagios').select('*, empresas(*)').eq('aluno_id', user.id).single(), - supabase.from('presencas').select('*').eq('aluno_id', user.id) - ]); + const { data: profile, error } = await supabase + .from('profiles') + .select('*') + .eq('id', user.id) + .single(); - const dadosPerfil = perfilRes.data; - if (dadosPerfil?.data_nascimento) { - dadosPerfil.data_nascimento = formatarParaExibir(dadosPerfil.data_nascimento); + if (error) throw error; + + if (profile?.data_nascimento) { + profile.data_nascimento = formatarParaExibir(profile.data_nascimento); } - setPerfil({ ...dadosPerfil, email: user.email }); - const dadosEstagio = estagioRes.data; - setEstagio(dadosEstagio); - - if (presencasRes.data && dadosEstagio) { - const dataInicio = new Date(dadosEstagio.data_inicio); - const dataFim = new Date(dadosEstagio.data_fim); - - // Filtrar apenas presenças que ocorrem DENTRO do intervalo do estágio - const presencasValidas = presencasRes.data.filter((p: any) => { - const dataP = new Date(p.data); - return dataP >= dataInicio && dataP <= dataFim; - }); - - setContagemPresencas(presencasValidas.filter((p: any) => p.estado === 'presente').length); - setContagemFaltas(presencasValidas.filter((p: any) => p.estado === 'faltou').length); - } + setPerfil({ ...profile, email: user.email }); } catch (e) { - showAlert('Erro ao carregar dados.', 'error'); + showAlert('Erro ao carregar perfil.', 'error'); } finally { setLoading(false); } @@ -138,14 +102,16 @@ export default function PerfilAluno() { nome: perfil.nome, telefone: perfil.telefone, residencia: perfil.residencia, - data_nascimento: dataBD + data_nascimento: dataBD, + curso: perfil.curso, + n_escola: perfil.n_escola }).eq('id', perfil.id); if (error) throw error; setIsEditing(false); - showAlert('Perfil atualizado!', 'success'); + showAlert('Perfil guardado!', 'success'); } catch (e) { - showAlert('Verifica se a data está correta.', 'error'); + showAlert('Erro ao salvar. Verifica os campos.', 'error'); } }; @@ -154,10 +120,6 @@ export default function PerfilAluno() { router.replace('/'); }; - const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim); - const horasRealizadas = contagemPresencas * 7; - const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0; - if (loading) return ; return ( @@ -171,7 +133,7 @@ export default function PerfilAluno() { )} - + router.back()}> @@ -194,53 +156,51 @@ export default function PerfilAluno() { {perfil?.nome} - Nº Aluno: {perfil?.n_escola || '---'} + {perfil?.curso || 'Sem Curso'} • {perfil?.n_escola || '---'} - {/* PROGRESS CARD */} - - Progresso do Estágio - - - {horasRealizadas}h - Feitas - - - {Math.max(0, horasTotais - horasRealizadas)}h - Restam - - - {contagemFaltas} - Faltas - + {/* DADOS ACADÉMICOS */} + Informação Académica + + + + setPerfil({...perfil, n_escola: v})} + cores={cores} keyboardType="numeric" + /> + + + setPerfil({...perfil, curso: v})} + cores={cores} autoCapitalize="characters" + /> + - - - - {Math.round(progresso * 100)}% das {horasTotais}h úteis concluídas + - {/* INFO CARD */} + {/* DADOS PESSOAIS */} + Dados Pessoais setPerfil({...perfil, nome: v})} cores={cores} /> - - {/* Aumentado o flex da data para 1.2 para evitar corte */} - + setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} - cores={cores} - maxLength={10} - keyboardType="numeric" - placeholder="DD-MM-AAAA" + cores={cores} maxLength={10} keyboardType="numeric" /> - + setPerfil({...perfil, telefone: v})} keyboardType="phone-pad" cores={cores} /> @@ -248,31 +208,8 @@ export default function PerfilAluno() { setPerfil({...perfil, residencia: v})} cores={cores} /> - {/* ESTÁGIO CARD ATUALIZADO */} - - Informações do Estágio - - - Início - {formatarParaExibir(estagio?.data_inicio)} - - - Fim - {formatarParaExibir(estagio?.data_fim)} - - - Horário - {estagio?.horario || '09:00-17:00'} - - - - Empresa e Tutor - {estagio?.empresas?.nome} - {estagio?.empresas?.tutor_nome} - - - - + {/* ACÇÕES */} + router.push('/Aluno/redefenirsenha')}> @@ -321,20 +258,14 @@ const styles = StyleSheet.create({ 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: 20 }, + profileHeader: { alignItems: 'center', marginVertical: 15 }, avatarContainer: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' }, - avatar: { width: 70, height: 70, borderRadius: 35, alignItems: 'center', justifyContent: 'center', elevation: 4 }, - avatarLetter: { color: '#fff', fontSize: 28, fontWeight: '800' }, - userName: { fontSize: 20, fontWeight: '800', marginTop: 12 }, - userRole: { fontSize: 13, fontWeight: '500' }, + 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 }, - statsGrid: { flexDirection: 'row', justifyContent: 'space-around' }, - statBox: { alignItems: 'center' }, - statValor: { fontSize: 18, fontWeight: '800' }, - statLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase' }, - progressBarBase: { height: 8, borderRadius: 4, overflow: 'hidden' }, - progressBarFill: { height: '100%', borderRadius: 4 }, - progressText: { fontSize: 10, textAlign: 'center', marginTop: 8, fontWeight: '700' }, 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 }, diff --git a/app/Professor/Alunos/CriarAluno.tsx b/app/Professor/Alunos/CriarAluno.tsx index 074c802..44fd7b8 100644 --- a/app/Professor/Alunos/CriarAluno.tsx +++ b/app/Professor/Alunos/CriarAluno.tsx @@ -1,7 +1,7 @@ // app/Professor/Alunos/CriarAluno.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, @@ -18,21 +18,49 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// Função para calcular idade automaticamente +const calcularIdade = (data: string): string => { + if (!data || data.length < 10) return ''; + const hoje = new Date(); + const nascimento = new Date(data); + 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.toString() : ''; +}; + const CriarAluno = () => { const { isDarkMode } = useTheme(); const router = useRouter(); const [loading, setLoading] = useState(false); - // ESTADOS + // ESTADOS DE LOGIN const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno'); + + // ESTADOS DE PERFIL (Comuns) const [nome, setNome] = useState(''); const [residencia, setResidencia] = useState(''); const [telefone, setTelefone] = useState(''); + const [dataNascimento, setDataNascimento] = useState(''); // Formato AAAA-MM-DD + const [idade, setIdade] = useState(''); + + // ESTADOS ESPECÍFICOS const [ano, setAno] = useState(''); - const [nEscola, setNEscola] = useState(''); - const [curso, setCurso] = useState(''); - const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno'); + const [nEscola, setNEscola] = useState(''); + const [curso, setCurso] = useState(''); + const [setor, setSetor] = useState(''); + + // CAMPOS PARA EMPRESA (Tutor) + const [tutorNome, setTutorNome] = useState(''); + const [tutorTelefone, setTutorTelefone] = useState(''); + + // Atualiza idade sempre que a data de nascimento mudar + useEffect(() => { + const novaIdade = calcularIdade(dataNascimento); + if (novaIdade) setIdade(novaIdade); + }, [dataNascimento]); const cores = useMemo(() => ({ fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', @@ -42,50 +70,60 @@ const CriarAluno = () => { azul: '#2390a6', laranja: '#E38E00', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + placeholder: isDarkMode ? '#555' : '#A0AEC0' }), [isDarkMode]); const handleCriar = async () => { - if (!email || !password || !nome || (tipo === 'aluno' && (!nEscola || !ano || !curso))) { - Alert.alert("Atenção", "Preenche os campos obrigatórios para evitar que isto dê merda."); - return; - } - - if (password.length < 6) { - Alert.alert("Erro", "A password tem de ter pelo menos 6 caracteres."); + const emailLimpo = email.trim(); + + if (!emailLimpo || !password || !nome) { + Alert.alert("Atenção", "Obrigatório: Email, Password e Nome."); return; } setLoading(true); try { - // 1. Criar Utilizador no Auth + // 1. Criar Auth User + // IMPORTANTE: Se o "Confirm Email" estiver ativo no Supabase, + // o signUp não inicia sessão automaticamente. const { data: authData, error: authError } = await supabase.auth.signUp({ - email, + email: emailLimpo, password, - options: { data: { nome, tipo } } + options: { + data: { nome, tipo }, + emailRedirectTo: undefined + } }); if (authError) throw authError; const user = authData.user; - if (!user) throw new Error("Erro ao gerar ID."); + + if (!user) { + Alert.alert("Verificação", "Utilizador criado. Verifique o email para ativar a conta."); + router.back(); + return; + } - // 2. Tabela 'profiles' + // 2. Inserir em PROFILES const { error: profileError } = await supabase .from('profiles') - .upsert({ + .insert([{ id: user.id, nome, - email, + email: emailLimpo, residencia, telefone, - n_escola: tipo === 'aluno' ? nEscola : null, - curso: tipo === 'aluno' ? curso.toUpperCase() : null, - tipo - }); + idade: idade ? parseInt(idade) : null, + data_nascimento: dataNascimento || null, + tipo, + n_escola: tipo !== 'professor' ? nEscola : null, + curso: tipo === 'aluno' ? curso : (tipo === 'professor' ? curso : setor) + }]); if (profileError) throw profileError; - // 3. Tabela 'alunos' (SÓ SE FOR ALUNO) + // 3. Inserir na tabela específica de ALUNOS if (tipo === 'aluno') { const { error: alunoError } = await supabase .from('alunos') @@ -93,19 +131,32 @@ const CriarAluno = () => { id: user.id, nome, n_escola: nEscola, - ano: parseInt(ano), + ano: ano ? parseInt(ano) : null, turma_curso: curso.toUpperCase() }]); if (alunoError) throw alunoError; } - Alert.alert("Sucesso", `${tipo.toUpperCase()} criado com sucesso!`, [ - { text: "OK", onPress: () => router.back() } - ]); + // 4. Se for EMPRESA + if (tipo === 'empresa') { + const { error: empresaError } = await supabase + .from('empresas') + .insert([{ + nome, + nif: nEscola, + setor, + tutor_nome: tutorNome, + tutor_telefone: tutorTelefone, + user_id: user.id + }]); + } + + Alert.alert("Sucesso", "Novo registo concluído com sucesso!"); + router.back(); } catch (err: any) { + Alert.alert("Erro ao criar conta", err.message); console.error(err); - Alert.alert("Erro no Registo", err.message); } finally { setLoading(false); } @@ -115,79 +166,118 @@ const CriarAluno = () => { + + + router.back()} style={[styles.backBtn, { borderColor: cores.borda }]}> + + + Novo Registo + + - - router.back()} style={[styles.backBtn, { borderColor: cores.borda }]}> - - - - Novo Registo - Criar utilizador no sistema - - - {/* SELETOR DE TIPO */} - Tipo de Utilizador {(['aluno', 'professor', 'empresa'] as const).map((item) => ( setTipo(item)} > - - {item.charAt(0).toUpperCase() + item.slice(1)} + + {item.toUpperCase()} ))} - Dados de Acesso + - Informação Geral + + + + + + {idade ? `${idade} anos` : 'Idade'} + + + + - - {/* CAMPOS ESPECÍFICOS PARA ALUNO */} + {/* CAMPOS DINÂMICOS */} {tipo === 'aluno' && ( <> - Dados Escolares + + + )} + + {tipo === 'professor' && ( + <> + + + + )} + + {tipo === 'empresa' && ( + <> + + + + + + + )} @@ -195,10 +285,10 @@ const CriarAluno = () => { - {loading ? : REGISTAR {tipo.toUpperCase()}} + {loading ? : REGISTAR NO SISTEMA} @@ -208,20 +298,22 @@ const CriarAluno = () => { ); }; +const SectionHeader = ({ title, cores }: any) => ( + {title} +); + const styles = StyleSheet.create({ - scroll: { padding: 24, paddingBottom: 60 }, - header: { flexDirection: 'row', alignItems: 'center', marginBottom: 25 }, - backBtn: { width: 45, height: 45, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center', marginRight: 15 }, - title: { fontSize: 24, fontWeight: 'bold' }, - subtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase' }, - sectionTitle: { fontSize: 13, fontWeight: 'bold', marginTop: 20, marginBottom: 10, opacity: 0.6 }, + scroll: { padding: 24, paddingBottom: 80 }, + header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15, gap: 15 }, + backBtn: { width: 45, height: 45, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + title: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, + sectionTitle: { fontSize: 11, fontWeight: '900', marginTop: 25, marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }, selectorContainer: { flexDirection: 'row', gap: 10, marginBottom: 10 }, - selectorBtn: { flex: 1, height: 45, borderRadius: 10, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, - selectorText: { fontSize: 13, fontWeight: 'bold' }, - form: { gap: 12 }, - input: { height: 55, borderRadius: 15, borderWidth: 1, paddingHorizontal: 15, fontSize: 16 }, - submitBtn: { height: 60, borderRadius: 15, marginTop: 30, justifyContent: 'center', alignItems: 'center' }, - submitBtnText: { color: '#fff', fontSize: 16, fontWeight: 'bold' }, + selectorBtn: { flex: 1, height: 48, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + form: { gap: 10 }, + input: { height: 55, borderRadius: 16, borderWidth: 1, paddingHorizontal: 18, fontSize: 15, fontWeight: '600' }, + submitBtn: { height: 62, borderRadius: 20, marginTop: 40, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, + submitBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', letterSpacing: 1 }, }); export default CriarAluno; \ No newline at end of file diff --git a/app/Professor/Alunos/DetalhesAluno.tsx b/app/Professor/Alunos/DetalhesAluno.tsx index 5a17bb1..d5e1ac4 100644 --- a/app/Professor/Alunos/DetalhesAluno.tsx +++ b/app/Professor/Alunos/DetalhesAluno.tsx @@ -4,11 +4,14 @@ import { useLocalSearchParams, useRouter } from 'expo-router'; import { memo, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, + Alert, Linking, + Modal, ScrollView, StatusBar, StyleSheet, Text, + TextInput, TouchableOpacity, View } from 'react-native'; @@ -16,75 +19,146 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// --- UTILITÁRIOS --- +const calcularIdade = (dataNascimento: string) => { + if (!dataNascimento) return null; + const hoje = new Date(); + const nascimento = new Date(dataNascimento); + let idade = hoje.getFullYear() - nascimento.getFullYear(); + const m = hoje.getMonth() - nascimento.getMonth(); + if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) { + idade--; + } + return idade; +}; + +// --- TIPAGENS --- +interface AlunoEditForm { + nome: string; + n_escola: string; + turma_curso: string; + telefone: string; + residencia: string; + data_nascimento: string; + email: string; +} + const DetalhesAlunos = memo(() => { const router = useRouter(); const params = useLocalSearchParams(); const { isDarkMode } = useTheme(); const insets = useSafeAreaInsets(); - const azulEPVC = '#2390a6'; - const laranjaEPVC = '#E38E00'; + const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null; + const [aluno, setAluno] = useState(null); + const [loading, setLoading] = useState(true); + const [modalVisible, setModalVisible] = useState(false); + const [saving, setSaving] = useState(false); + + const [editForm, setEditForm] = useState({ + nome: '', + n_escola: '', + turma_curso: '', + telefone: '', + residencia: '', + data_nascimento: '', + email: '' + }); const cores = useMemo(() => ({ fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', card: isDarkMode ? '#161618' : '#F8FAFC', texto: isDarkMode ? '#F8FAFC' : '#1A365D', secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: azulEPVC, - laranja: laranjaEPVC, + azul: '#2390a6', + laranja: '#E38E00', azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#10B981', + inputFundo: isDarkMode ? '#252525' : '#EDF2F7' }), [isDarkMode]); - const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null; - const [aluno, setAluno] = useState(null); - const [loading, setLoading] = useState(true); - const fetchAluno = async () => { + if (!alunoId) return; try { setLoading(true); - const { data, error } = await supabase + + const { data: alunoData, error: alunoError } = await supabase .from('alunos') - .select(` - id, nome, n_escola, turma_curso, - profiles!alunos_profile_id_fkey ( email, telefone, residencia, idade ), - estagios ( - id, data_inicio, data_fim, horas_diarias, - empresas ( nome, tutor_nome, tutor_telefone ) - ) - `) + .select(`*, estagios(*, empresas(*))`) + .eq('id', alunoId) + .single(); + + if (alunoError) throw alunoError; + + const { data: perfilData } = await supabase + .from('profiles') + .select('*') .eq('id', alunoId) .single(); - if (error) throw error; - let listaHorarios: string[] = []; - if (data?.estagios?.[0]?.id) { + const estagioAtivo = Array.isArray(alunoData.estagios) ? alunoData.estagios[0] : alunoData.estagios; + + if (estagioAtivo?.id) { const { data: hData } = await supabase .from('horarios_estagio') - .select('hora_inicio, hora_fim') - .eq('estagio_id', data.estagios[0].id); - + .select('*') + .eq('estagio_id', estagioAtivo.id); if (hData) { - listaHorarios = hData.map(h => `${h.hora_inicio?.slice(0, 5)} - ${h.hora_fim?.slice(0, 5)}`); + listaHorarios = hData.map((h: any) => `${h.hora_inicio?.slice(0, 5)}h - ${h.hora_fim?.slice(0, 5)}h`); } } - setAluno({ - ...data, - perfil: Array.isArray(data.profiles) ? data.profiles[0] : data.profiles, - estagio: Array.isArray(data.estagios) ? data.estagios[0] : data.estagios, - horarios: listaHorarios + const infoCompleta = { ...alunoData, perfil: perfilData, estagio: estagioAtivo, horarios: listaHorarios }; + setAluno(infoCompleta); + + setEditForm({ + nome: alunoData.nome || '', + n_escola: String(alunoData.n_escola || ''), + turma_curso: alunoData.turma_curso || '', + telefone: perfilData?.telefone || '', + residencia: perfilData?.residencia || '', + data_nascimento: perfilData?.data_nascimento || '', + email: perfilData?.email || '' }); + } catch (err: any) { - console.log('Erro:', err.message); + console.error(err); + Alert.alert("Erro", "Falha ao carregar dados."); } finally { setLoading(false); } }; - useEffect(() => { if (alunoId) fetchAluno(); }, [alunoId]); + const handleUpdate = async () => { + try { + setSaving(true); + const { error: err1 } = await supabase.from('alunos').update({ + nome: editForm.nome, + n_escola: editForm.n_escola, + turma_curso: editForm.turma_curso + }).eq('id', alunoId); + + const { error: err2 } = await supabase.from('profiles').update({ + telefone: editForm.telefone, + residencia: editForm.residencia, + data_nascimento: editForm.data_nascimento, + email: editForm.email + }).eq('id', alunoId); + + if (err1 || err2) throw new Error("Erro na gravação dos dados"); + + Alert.alert("Sucesso", "Dados atualizados!"); + setModalVisible(false); + fetchAluno(); + } catch (err: any) { + Alert.alert("Erro", "Não foi possível guardar. Verifica a ligação."); + } finally { + setSaving(false); + } + }; + + useEffect(() => { fetchAluno(); }, [alunoId]); if (loading) return ( @@ -97,20 +171,19 @@ const DetalhesAlunos = memo(() => { - {/* HEADER CLEAN */} + {/* HEADER */} router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}> Ficha do Aluno - - + setModalVisible(true)} style={[styles.btnAction, { borderColor: cores.borda }]}> + - {/* PERFIL MINIMALISTA */} {aluno?.nome?.charAt(0)} @@ -121,17 +194,27 @@ const DetalhesAlunos = memo(() => { - {/* DADOS PESSOAIS CARD */} + {/* INFORMAÇÕES PESSOAIS */} - Linking.openURL(`mailto:${aluno?.perfil?.email}`)} /> - Linking.openURL(`tel:${aluno?.perfil?.telefone}`)} /> + + Linking.openURL(`mailto:${aluno.perfil.email}`) : null} + /> + Linking.openURL(`tel:${aluno.perfil.telefone}`) : null} + /> - {/* SECÇÃO ESTÁGIO */} + {/* ESTÁGIO */} Plano de Estágio @@ -143,70 +226,102 @@ const DetalhesAlunos = memo(() => { ATIVO - - {aluno?.estagio?.empresas?.nome || 'Empresa Indefinida'} + {aluno?.estagio?.empresas?.nome || 'Empresa'} - TUTOR NA EMPRESA - {aluno?.estagio?.empresas?.tutor_nome || 'Não definido'} + TUTOR + {aluno?.estagio?.empresas?.tutor_nome || 'N/A'} {aluno?.estagio?.empresas?.tutor_telefone || '-'} HORÁRIOS - {aluno?.horarios && aluno.horarios.length > 0 ? ( - aluno.horarios.map((h: string, i: number) => ( - {h} - )) - ) : ( - Não registado - )} + {aluno?.horarios?.length > 0 ? ( + aluno.horarios.map((h: string, i: number) => {h}) + ) : Não definido} - - - {/* PERÍODO CORRIGIDO: DUAS COLUNAS COM ÍCONE */} - - - INÍCIO - {aluno?.estagio?.data_inicio} - + INÍCIO + {aluno?.estagio?.data_inicio || '-'} - - FIM PREVISTO - {aluno?.estagio?.data_fim} - - + FIM + {aluno?.estagio?.data_fim || '-'} ) : ( - Sem estágio atribuído )} + + {/* MODAL DE EDIÇÃO */} + + + + + Editar Aluno + setModalVisible(false)}> + + + + + + setEditForm({...editForm, nome: t})} cores={cores} /> + setEditForm({...editForm, n_escola: t})} cores={cores} keyboard="numeric" /> + setEditForm({...editForm, turma_curso: t})} cores={cores} /> + setEditForm({...editForm, email: t})} cores={cores} keyboard="email-address" /> + setEditForm({...editForm, telefone: t})} cores={cores} keyboard="phone-pad" /> + setEditForm({...editForm, data_nascimento: t})} cores={cores} keyboard="numeric" /> + setEditForm({...editForm, residencia: t})} cores={cores} /> + + + {saving ? : Guardar Alterações} + + + + + + ); }); +// --- COMPONENTES AUXILIARES --- +interface EditInputProps { + label: string; + value: string; + onChange: (t: string) => void; + cores: any; + keyboard?: any; +} + +const EditInput = ({ label, value, onChange, cores, keyboard = "default" }: EditInputProps) => ( + + {label} + + +); + const DetailRow = ({ icon, label, value, cores, ultimo, onPress }: any) => ( - - + + {label} {value || '-'} - {onPress && } + {onPress && } ); @@ -220,34 +335,37 @@ const styles = StyleSheet.create({ avatar: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center' }, avatarTxt: { fontSize: 26, fontWeight: '900' }, alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - alunoCurso: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase' }, + alunoCurso: { fontSize: 13, fontWeight: '800' }, infoCard: { borderRadius: 25, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 5, marginBottom: 30 }, - row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 15 }, - rowLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 }, - rowValue: { fontSize: 15, fontWeight: '700' }, + row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 }, + rowLabel: { fontSize: 9, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 }, + rowValue: { fontSize: 14, fontWeight: '700' }, sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, - sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginRight: 15 }, - sectionLine: { flex: 1, height: 1, opacity: 0.3 }, - estagioCard: { padding: 25, borderRadius: 32, elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 }, - estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }, - statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, - statusText: { color: '#fff', fontSize: 10, fontWeight: '900' }, - empresaNome: { color: '#fff', fontSize: 24, fontWeight: '900', marginBottom: 20 }, + sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginRight: 15 }, + sectionLine: { flex: 1, height: 1, opacity: 0.2 }, + estagioCard: { padding: 25, borderRadius: 30 }, + estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 }, + statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 }, + statusText: { color: '#fff', fontSize: 9, fontWeight: '900' }, + empresaNome: { color: '#fff', fontSize: 22, fontWeight: '900', marginBottom: 15 }, tutorInfo: { marginBottom: 15 }, - miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, fontWeight: '900', marginBottom: 4 }, - tutorNome: { color: '#fff', fontSize: 16, fontWeight: '800' }, - tutorTel: { color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: '600' }, - horarioBox: { marginBottom: 20, backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15 }, - horarioTxt: { color: '#fff', fontSize: 14, fontWeight: '700' }, - estagioDivider: { height: 1, backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 20 }, - - // ESTILOS DO RODAPÉ CORRIGIDOS + miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 8, fontWeight: '900' }, + tutorNome: { color: '#fff', fontSize: 15, fontWeight: '800' }, + tutorTel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 }, + horarioBox: { backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15, marginBottom: 15 }, + horarioTxt: { color: '#fff', fontSize: 13, fontWeight: '700' }, estagioFooter: { flexDirection: 'row', justifyContent: 'space-between' }, - periodoCol: { flex: 1, flexDirection: 'row', alignItems: 'center' }, - footerVal: { color: '#fff', fontSize: 14, fontWeight: '800' }, - - noEstagio: { padding: 30, borderRadius: 25, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center', gap: 10 }, - noEstagioTxt: { fontWeight: '700', fontSize: 14 } + periodoCol: { flex: 1 }, + footerVal: { color: '#fff', fontSize: 13, fontWeight: '800' }, + noEstagio: { padding: 20, borderRadius: 20, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center' }, + noEstagioTxt: { fontWeight: '700', fontSize: 14 }, + modalContainer: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, + modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 25, maxHeight: '90%' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, + modalTitle: { fontSize: 20, fontWeight: '900' }, + input: { borderRadius: 12, padding: 12, fontSize: 15, fontWeight: '600' }, + btnSave: { borderRadius: 15, padding: 16, alignItems: 'center', marginTop: 20, marginBottom: 40 }, + btnSaveTxt: { color: '#fff', fontWeight: '900', fontSize: 16 } }); export default DetalhesAlunos; \ No newline at end of file diff --git a/app/Professor/Alunos/ListaAlunos.tsx b/app/Professor/Alunos/ListaAlunos.tsx index 2a01e13..6565cc2 100644 --- a/app/Professor/Alunos/ListaAlunos.tsx +++ b/app/Professor/Alunos/ListaAlunos.tsx @@ -25,7 +25,6 @@ export interface Aluno { nome: string; n_escola: string; turma: string; - tem_estagio?: boolean; } interface TurmaAgrupada { @@ -48,19 +47,15 @@ const ListaAlunosProfessor = memo(() => { const [alunoParaEliminar, setAlunoParaEliminar] = useState(null); const [isDeleting, setIsDeleting] = useState(false); - const azulEPVC = '#2390a6'; - const laranjaEPVC = '#E38E00'; - const cores = useMemo(() => ({ fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', card: isDarkMode ? '#161618' : '#F8FAFC', texto: isDarkMode ? '#F8FAFC' : '#1A365D', secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: azulEPVC, - laranja: laranjaEPVC, + azul: '#2390a6', + laranja: '#E38E00', azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#10B981', vermelho: '#EF4444', vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', }), [isDarkMode]); @@ -70,7 +65,7 @@ const ListaAlunosProfessor = memo(() => { setLoading(true); const { data, error } = await supabase .from('alunos') - .select(`id, nome, n_escola, ano, turma_curso, estagios(id)`) + .select(`id, nome, n_escola, ano, turma_curso`) .order('ano', { ascending: false }) .order('nome', { ascending: true }); @@ -85,13 +80,15 @@ const ListaAlunosProfessor = memo(() => { nome: item.nome, n_escola: item.n_escola, turma: nomeTurma, - tem_estagio: item.estagios && item.estagios.length > 0 }); }); - setTurmas(Object.keys(agrupadas).sort((a, b) => b.localeCompare(a)).map(nome => ({ nome, alunos: agrupadas[nome] }))); + setTurmas(Object.keys(agrupadas).sort((a, b) => b.localeCompare(a)).map(nome => ({ + nome, + alunos: agrupadas[nome] + }))); } catch (err) { - console.error(err); + console.error("Erro ao carregar lista:", err); } finally { setLoading(false); setRefreshing(false); @@ -100,16 +97,37 @@ const ListaAlunosProfessor = memo(() => { const confirmarEliminacao = async () => { if (!alunoParaEliminar) return; + try { setIsDeleting(true); - const { error } = await supabase.from('alunos').delete().eq('id', alunoParaEliminar.id); + + // APAGAR NO PROFILES (O Cascade do SQL trata das tabelas 'alunos' e 'estagios') + const { data, error } = await supabase + .from('profiles') + .delete() + .eq('id', alunoParaEliminar.id) + .select(); + if (error) throw error; - + + // Se o data vier vazio, o RLS bloqueou o delete no profiles + if (!data || data.length === 0) { + throw new Error("O servidor recusou apagar o perfil. Verifica se as políticas RLS na tabela 'profiles' permitem DELETE."); + } + + // Sucesso no banco -> Atualizar UI local + setTurmas(prev => prev.map(turma => ({ + ...turma, + alunos: turma.alunos.filter(a => a.id !== alunoParaEliminar.id) + })).filter(t => t.alunos.length > 0)); + setShowDeleteModal(false); setAlunoParaEliminar(null); - fetchAlunos(); - } catch (err) { - Alert.alert("Erro", "Não foi possível eliminar o aluno."); + Alert.alert("Sucesso", "Aluno e todos os dados vinculados foram eliminados."); + + } catch (err: any) { + console.error("ERRO AO APAGAR:", err); + Alert.alert("Erro Crítico", err.message); } finally { setIsDeleting(false); } @@ -152,13 +170,13 @@ const ListaAlunosProfessor = memo(() => { - {/* SEARCH */} + {/* PESQUISA */} { {aluno.nome} Nº {aluno.n_escola} - + ))} @@ -207,18 +225,18 @@ const ListaAlunosProfessor = memo(() => { /> )} - {/* MODAL DE ELIMINAÇÃO CUSTOMIZADO */} + {/* MODAL DE ELIMINAÇÃO */} - + - Eliminar Aluno? + Eliminar Permanentemente? - Estás prestes a apagar {alunoParaEliminar?.nome}. - Esta ação é irreversível e **vai dar merda** se não tiveres a certeza! + Estás prestes a apagar o perfil de {alunoParaEliminar?.nome}. + Isto removerá o acesso à app, dados escolares e estágios. **Vai dar merda** se apagares por engano! @@ -237,7 +255,7 @@ const ListaAlunosProfessor = memo(() => { {isDeleting ? ( ) : ( - Eliminar + Eliminar Tudo )} @@ -245,9 +263,10 @@ const ListaAlunosProfessor = memo(() => { + {/* BOTÃO FLUTUANTE */} router.push('/Professor/Alunos/CriarAluno')} // Altera esta linha + onPress={() => router.push('/Professor/Alunos/CriarAluno')} > Novo Aluno @@ -277,13 +296,12 @@ const styles = StyleSheet.create({ alunoInfo: { flex: 1, marginLeft: 15 }, alunoNome: { fontSize: 16, fontWeight: '800' }, idText: { fontSize: 13, fontWeight: '600' }, - fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22 }, + fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5 }, fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10 }, - // Estilos do Modal modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', alignItems: 'center', padding: 30 }, modalCard: { width: '100%', borderRadius: 32, padding: 24, alignItems: 'center' }, warningIconBox: { width: 70, height: 70, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - modalTitle: { fontSize: 22, fontWeight: '900', marginBottom: 10 }, + modalTitle: { fontSize: 20, fontWeight: '900', marginBottom: 10, textAlign: 'center' }, modalDesc: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 25 }, modalButtons: { flexDirection: 'row', gap: 12, width: '100%' }, btnModal: { flex: 1, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index 23be623..a4b8d3c 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -49,17 +49,13 @@ export default function PerfilProfessor() { ]).start(() => setAlertConfig(null)); }, []); - // Cores EPVC - const azulEPVC = '#2390a6'; - const laranjaEPVC = '#E38E00'; - const cores = useMemo(() => ({ fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', card: isDarkMode ? '#161618' : '#F8FAFC', texto: isDarkMode ? '#F8FAFC' : '#1A365D', secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: azulEPVC, - laranja: laranjaEPVC, + azul: '#2390a6', + laranja: '#E38E00', azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FFF5F5', vermelho: '#EF4444', @@ -72,18 +68,36 @@ export default function PerfilProfessor() { async function carregarPerfil() { try { setLoading(true); - const { data: { user } } = await supabase.auth.getUser(); - if (user) { - const { data, error } = await supabase - .from('profiles') - .select('*') - .eq('id', user.id) - .single(); - if (error) throw error; - setPerfil(data); + + // 1. Obter a sessão atual de forma limpa + const { data: { session }, error: sessionError } = await supabase.auth.getSession(); + + if (sessionError || !session) { + router.replace('/'); + return; } + + // 2. Buscar o perfil filtrando pelo ID da sessão e garantindo que o tipo é PROFESSOR + // Isso impede que, se a sessão mudar para aluno, os dados apareçam aqui + const { data, error } = await supabase + .from('profiles') + .select('*') + .eq('id', session.user.id) + .eq('tipo', 'professor') // Filtro de segurança + .single(); + + if (error || !data) { + // Se for um aluno a tentar aceder a esta página de professor, expulsamos + showAlert('Acesso negado ou perfil não encontrado.', 'error'); + await supabase.auth.signOut(); + router.replace('/'); + return; + } + + setPerfil(data); } catch (error: any) { - showAlert('Não foi possível carregar os dados.', 'error'); + console.error(error); + showAlert('Erro ao carregar dados.', 'error'); } finally { setLoading(false); } @@ -148,7 +162,7 @@ export default function PerfilProfessor() { router.back()}> - Perfil + O Meu Perfil editando ? guardarPerfil() : setEditando(true)} @@ -164,34 +178,29 @@ export default function PerfilProfessor() { {perfil?.nome?.charAt(0).toUpperCase()} - {editando && ( - - - - )} {perfil?.nome} - {perfil?.curso || 'Professor'} + PROFESSOR • {perfil?.curso} - setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} /> - setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} /> - + - setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} /> - setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} /> @@ -203,7 +212,7 @@ export default function PerfilProfessor() { - Alterar palavra-passe + Segurança da Conta @@ -212,13 +221,13 @@ export default function PerfilProfessor() { - Terminar Sessão + Sair do Sistema {editando && ( { setEditando(false); carregarPerfil(); }}> - Cancelar Alterações + Reverter Alterações )} @@ -231,7 +240,7 @@ export default function PerfilProfessor() { const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( {label} - + @@ -250,12 +259,11 @@ const styles = StyleSheet.create({ scrollContent: { paddingHorizontal: 24, paddingBottom: 50 }, profileHeader: { alignItems: 'center', marginVertical: 35 }, avatarBorder: { padding: 4, borderRadius: 100, borderWidth: 2, position: 'relative' }, - avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 }, + avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center' }, avatarLetter: { color: '#fff', fontSize: 36, fontWeight: '900' }, - editBadge: { position: 'absolute', bottom: 0, right: 0, width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 3, borderColor: '#fff' }, userName: { fontSize: 24, fontWeight: '900', marginTop: 15, letterSpacing: -0.5 }, roleBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 }, - userRole: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + userRole: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, card: { borderRadius: 28, padding: 24, marginBottom: 20, borderWidth: 1 }, inputWrapper: { marginBottom: 18 }, inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },