diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index eaac02b..af12910 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -20,7 +20,6 @@ LocaleConfig.locales['pt'] = { }; LocaleConfig.defaultLocale = 'pt'; -// --- FUNÇÃO PARA CALCULAR FERIADOS (Nacionais + Vila do Conde) --- const getFeriadosMap = (ano: number) => { const f: Record = { [`${ano}-01-01`]: "Ano Novo", @@ -107,12 +106,16 @@ const AlunoHome = memo(() => { const diaSemana = data.getDay(); const ehFimDeSemana = diaSemana === 0 || diaSemana === 6; const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim; + + // CORREÇÃO: Verifica se o dia selecionado é exatamente HOJE + const ehHoje = selectedDate === hojeStr; const ehFuturo = selectedDate > hojeStr; const nomeFeriado = feriadosMap[selectedDate]; return { valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado, - podeMarcarPresenca: !ehFimDeSemana && !foraDoIntervalo && !ehFuturo && !nomeFeriado, + // Só permite presença se for o dia atual + podeMarcarPresenca: ehHoje && !foraDoIntervalo && !nomeFeriado, ehFuturo, nomeFeriado }; @@ -133,7 +136,7 @@ const AlunoHome = memo(() => { }, [presencas, faltas, sumarios, faltasJustificadas, selectedDate, listaFeriados]); const handlePresenca = async () => { - if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "Data inválida."); + if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "A presença só pode ser marcada no próprio dia."); const novas = { ...presencas, [selectedDate]: true }; setPresencas(novas); await AsyncStorage.setItem('@presencas', JSON.stringify(novas)); @@ -202,7 +205,7 @@ const AlunoHome = memo(() => { { setSelectedDate(day.dateString); setEditandoSumario(false); }} diff --git a/app/Aluno/perfil.tsx b/app/Aluno/perfil.tsx index 1836295..d08a8d9 100644 --- a/app/Aluno/perfil.tsx +++ b/app/Aluno/perfil.tsx @@ -1,234 +1,101 @@ -//PÁGINA PERFIL DO ALUNO - -import { Ionicons } from '@expo/vector-icons'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { useRouter } from 'expo-router'; import { useEffect, useState } from 'react'; -import { - Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, - Text, TouchableOpacity, View -} from 'react-native'; +import { ActivityIndicator, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useTheme } from '../../themecontext'; +import { supabase } from '../lib/supabase'; -export default function Perfil() { +export default function PerfilAluno() { const { isDarkMode } = useTheme(); const router = useRouter(); + const [loading, setLoading] = useState(true); + const [perfil, setPerfil] = useState(null); + const [estagio, setEstagio] = useState(null); - // Estados para dados dinâmicos e estatísticas - const [datas, setDatas] = useState({ inicio: '05/01/2026', fim: '30/05/2026' }); - const [stats, setStats] = useState({ - horasConcluidas: 0, - faltasTotais: 0, - faltasJustificadas: 0, - horasFaltam: 300 - }); + useEffect(() => { + carregarDados(); + }, []); + + async function carregarDados() { + try { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return; + + // 1. Dados do Perfil + const { data: prof } = await supabase.from('profiles').select('*').eq('id', user.id).single(); + setPerfil(prof); + + // 2. Dados do Estágio e Empresa (Relacionados) + const { data: est } = await supabase + .from('estagios') + .select('*, empresas(*)') + .eq('aluno_id', user.id) + .single(); + setEstagio(est); + + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + } const themeStyles = { fundo: isDarkMode ? '#121212' : '#f1f3f5', card: isDarkMode ? '#1e1e1e' : '#fff', texto: isDarkMode ? '#fff' : '#000', textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - borda: isDarkMode ? '#333' : '#f1f3f5', }; - useEffect(() => { - const carregarECalcular = async () => { - try { - const [config, presencasRaw, faltasRaw, justRaw] = await Promise.all([ - AsyncStorage.getItem('@dados_estagio'), - AsyncStorage.getItem('@presencas'), - AsyncStorage.getItem('@faltas'), - AsyncStorage.getItem('@justificacoes') - ]); - - // 1. Carregar Configurações de Datas - if (config) { - const p = JSON.parse(config); - setDatas({ - inicio: p.inicio || '05/01/2026', - fim: p.fim || '30/05/2026' - }); - } - - // 2. Calcular estatísticas baseadas nos objetos do calendário - const presencas = presencasRaw ? JSON.parse(presencasRaw) : {}; - const faltas = faltasRaw ? JSON.parse(faltasRaw) : {}; - const justificacoes = justRaw ? JSON.parse(justRaw) : {}; - - const totalDiasPresenca = Object.keys(presencas).length; - const totalFaltas = Object.keys(faltas).length; - const totalJustificadas = Object.keys(justificacoes).length; - - const horasFeitas = totalDiasPresenca * 7; // 7h por dia - const totalHorasEstagio = 300; - - setStats({ - horasConcluidas: horasFeitas, - faltasTotais: totalFaltas, - faltasJustificadas: totalJustificadas, - horasFaltam: Math.max(0, totalHorasEstagio - horasFeitas) - }); - - } catch (e) { - console.error("Erro ao sincronizar dados do perfil", e); - } - }; - carregarECalcular(); - }, []); + if (loading) return ; return ( - - - - router.back()} - > - - - - Perfil do Aluno - - - + + O Meu Perfil - - - {/* Dados Pessoais - RESTAURADO TOTALMENTE */} + {/* Dados Pessoais vindos da tabela PROFILES */} Dados Pessoais - - Nome - Ricardo Gomes - - Idade - 17 anos - - Residência - Junqueira, Vila do Conde - - Telemóvel - 915783648 + + + + - {/* NOVA SECÇÃO: Assiduidade Dinâmica */} + {/* Dados da Empresa vindos da tabela EMPRESAS via Estágio */} - Assiduidade - - - Faltas Totais - {stats.faltasTotais} - - - Justificadas - {stats.faltasJustificadas} - - - Ñ Justif. - - {stats.faltasTotais - stats.faltasJustificadas} - - - - - - {/* Empresa de Estágio - RESTAURADO TOTALMENTE */} - - Empresa de Estágio - - Curso - Técnico de Informática - - Empresa - Tech Solutions, Lda - - Morada - Rua das papoilas, nº67 - - Tutor - Nicolau de Sousa - - Telemóvel - 917892748 - - - {/* Dados do Estágio - HORAS DINÂMICAS */} - - Dados do Estágio - - Início do Estágio - {datas.inicio} - - Fim do Estágio - {datas.fim} - - - - Totais - 300h - - - Concluídas - {stats.horasConcluidas}h - - - Faltam - {stats.horasFaltam}h - - - - Horário Semanal - - - - Período - Horário - - - - Manhã - 09:30 - 13:00 - - - - Almoço - 13:00 - 14:30 - - - - Tarde - 14:30 - 17:30 - - - - Total: 7 horas diárias por presença + Local de Estágio + + + + + { await supabase.auth.signOut(); router.replace('/'); }} + > + Sair da Conta + ); } +function LabelValor({ label, valor, theme }: any) { + return ( + + {label} + {valor || '---'} + + ); +} + const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 }, - btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 }, - spacer: { width: 40 }, - container: { padding: 20, gap: 20, paddingBottom: 40 }, - tituloGeral: { fontSize: 22, fontWeight: 'bold' }, - card: { padding: 20, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 }, - tituloCard: { fontSize: 18, fontWeight: 'bold', color: '#0d6efd', textAlign: 'center', marginBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f3f5', paddingBottom: 8 }, - label: { marginTop: 12, fontSize: 13 }, - valor: { fontSize: 16, fontWeight: '600' }, - labelHorario: { fontSize: 16, fontWeight: 'bold', marginTop: 20, marginBottom: 10, textAlign: 'center' }, - tabela: { borderWidth: 1, borderRadius: 8, overflow: 'hidden' }, - linhaTab: { flexDirection: 'row', borderBottomWidth: 1, paddingVertical: 8, alignItems: 'center' }, - celulaHeader: { flex: 1, fontWeight: 'bold', textAlign: 'center', fontSize: 13 }, - celulaLabel: { flex: 1, paddingLeft: 12, fontSize: 14 }, - celulaValor: { flex: 1, textAlign: 'center', fontSize: 14, fontWeight: '600' }, - notaTotal: { textAlign: 'center', fontSize: 12, color: '#0d6efd', marginTop: 8, fontWeight: '500' }, - estatisticasHoras: { flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, paddingBottom: 15, marginTop: 5 }, - rowStats: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 }, - itemStat: { alignItems: 'center', flex: 1 } + safe: { flex: 1 }, + container: { padding: 20, gap: 20 }, + tituloGeral: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 }, + card: { padding: 20, borderRadius: 16, elevation: 2 }, + tituloCard: { fontSize: 14, fontWeight: 'bold', color: '#0d6efd', marginBottom: 15, textTransform: 'uppercase' }, + btnSair: { backgroundColor: '#dc3545', padding: 18, borderRadius: 15, alignItems: 'center', marginTop: 10 } }); \ No newline at end of file diff --git a/app/Professor/Alunos/DetalhesAluno.tsx b/app/Professor/Alunos/DetalhesAluno.tsx index 6cc4b64..bdd2f9e 100644 --- a/app/Professor/Alunos/DetalhesAluno.tsx +++ b/app/Professor/Alunos/DetalhesAluno.tsx @@ -1,3 +1,4 @@ +// app/Professor/Alunos/DetalhesAluno.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { memo, useEffect, useState } from 'react'; @@ -13,6 +14,18 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// Definindo a interface para o estado do aluno +interface AlunoEstado { + id: string; + nome: string; + n_escola: string; + turma_curso: string; + email: string; + telefone: string; + residencia: string; + idade: string; +} + const DetalhesAlunos = memo(() => { const router = useRouter(); const params = useLocalSearchParams(); @@ -32,7 +45,7 @@ const DetalhesAlunos = memo(() => { ? params.alunoId[0] : null; - const [aluno, setAluno] = useState(null); + const [aluno, setAluno] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -47,42 +60,45 @@ const DetalhesAlunos = memo(() => { try { setLoading(true); - // 1️⃣ Buscar dados do aluno - const { data: alunoData, error: alunoError } = await supabase + const { data, error } = await supabase .from('alunos') - .select('id, nome, n_escola, turma_curso') + .select(` + id, + nome, + n_escola, + turma_curso, + profiles!alunos_profile_id_fkey ( + email, + telefone, + residencia, + idade + ) + `) .eq('id', alunoId) .single(); - if (alunoError || !alunoData) { - console.log('Erro ao buscar aluno:', alunoError); - setAluno(null); + if (error) { + console.log('Erro ao buscar:', error.message); setLoading(false); return; } - // 2️⃣ Buscar dados do profile como array - const { data: perfilDataArray, error: perfilError } = await supabase - .from('profiles') - .select('email, telefone, residencia, idade') - .eq('n_escola', alunoData.n_escola); + if (data) { + // CORREÇÃO AQUI: Forçamos o TypeScript a tratar profiles como um objeto + // para que ele permita acessar email, telefone, etc. + const perfil = data.profiles as any; - if (perfilError) console.log('Erro ao buscar profile:', perfilError); - - // Pega o primeiro registro ou undefined - const perfilData = perfilDataArray?.[0]; - - // 3️⃣ Combinar dados - setAluno({ - id: alunoData.id, - nome: alunoData.nome, - n_escola: alunoData.n_escola, - turma_curso: alunoData.turma_curso, - email: perfilData?.email ?? '-', - telefone: perfilData?.telefone ?? '-', - residencia: perfilData?.residencia ?? '-', - idade: perfilData?.idade?.toString() ?? '-', - }); + setAluno({ + id: String(data.id), + nome: data.nome || 'Sem nome', + n_escola: String(data.n_escola || '-'), + turma_curso: data.turma_curso || '-', + email: perfil?.email ?? '-', + telefone: perfil?.telefone ?? '-', + residencia: perfil?.residencia ?? '-', + idade: perfil?.idade ? String(perfil.idade) : '-', + }); + } setLoading(false); } catch (err) { @@ -136,10 +152,10 @@ const DetalhesAlunos = memo(() => { }); const renderCampo = (label: string, valor: string, colors: any) => ( - <> + {label} {valor} - + ); export default DetalhesAlunos; @@ -147,10 +163,24 @@ export default DetalhesAlunos; const styles = StyleSheet.create({ safe: { flex: 1 }, center: { marginTop: 50, textAlign: 'center', fontSize: 16 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12 }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12 + }, titulo: { fontSize: 20, fontWeight: 'bold' }, container: { padding: 16 }, - card: { padding: 16, borderRadius: 12, elevation: 2 }, - label: { fontSize: 12, marginTop: 10 }, + card: { + padding: 16, + borderRadius: 12, + elevation: 2, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 } + }, + label: { fontSize: 12, marginBottom: 2, textTransform: 'uppercase', letterSpacing: 0.5 }, valor: { fontSize: 16, fontWeight: '600' }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/Estagios.tsx b/app/Professor/Alunos/Estagios.tsx index 588d63d..3cd3371 100644 --- a/app/Professor/Alunos/Estagios.tsx +++ b/app/Professor/Alunos/Estagios.tsx @@ -1,51 +1,39 @@ -// app/Professor/Estagios.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { - Alert, - Modal, + ActivityIndicator, + Alert, Modal, Platform, - SafeAreaView, - ScrollView, + SafeAreaView, ScrollView, StatusBar, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - View + StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; -import { useTheme } from '../../../themecontext'; // mesmo theme context das definições +import { useTheme } from '../../../themecontext'; +import { supabase } from '../../lib/supabase'; -interface Aluno { - id: number; - nome: string; +// --- Interfaces --- +interface Horario { + id?: number; + periodo: string; + hora_inicio: string; + hora_fim: string; } -interface Empresa { - id: number; - nome: string; -} +interface Aluno { id: string; nome: string; turma_curso: string; } +interface Empresa { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; } interface Estagio { - id: number; - alunoId: number; - empresaId: number; - alunoNome: string; - empresaNome: string; + id: string; + aluno_id: string; + empresa_id: string; + data_inicio: string; + data_fim: string; + horas_diarias?: string; + alunos: { nome: string; turma_curso: string }; + empresas: { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string }; } -// Dados simulados -const alunosData: Aluno[] = [ - { id: 1, nome: 'João Silva' }, - { id: 2, nome: 'Maria Fernandes' }, -]; - -const empresasData: Empresa[] = [ - { id: 1, nome: 'Empresa A' }, - { id: 2, nome: 'Empresa B' }, -]; - export default function Estagios() { const router = useRouter(); const { isDarkMode } = useTheme(); @@ -58,168 +46,319 @@ export default function Estagios() { border: isDarkMode ? '#343a40' : '#ced4da', azul: '#0d6efd', vermelho: '#dc3545', + inputBg: isDarkMode ? '#2c2c2c' : '#f8f9fa', }), [isDarkMode]); - // Estados + // --- Estados --- const [estagios, setEstagios] = useState([]); + const [alunos, setAlunos] = useState([]); + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(true); + const [modalVisible, setModalVisible] = useState(false); + const [passo, setPasso] = useState(1); const [alunoSelecionado, setAlunoSelecionado] = useState(null); const [empresaSelecionada, setEmpresaSelecionada] = useState(null); const [editandoEstagio, setEditandoEstagio] = useState(null); - const [searchText, setSearchText] = useState(''); + + const [dataInicio, setDataInicio] = useState(''); + const [dataFim, setDataFim] = useState(''); + const [horarios, setHorarios] = useState([]); - // Filtrar estágios por busca - const estagiosFiltrados = estagios.filter(e => - e.alunoNome.toLowerCase().includes(searchText.toLowerCase()) || - e.empresaNome.toLowerCase().includes(searchText.toLowerCase()) - ); + const [searchMain, setSearchMain] = useState(''); + const [searchAluno, setSearchAluno] = useState(''); + const [searchEmpresa, setSearchEmpresa] = useState(''); - const abrirModalNovo = () => { + // --- Cálculo de Horas Diárias --- + const totalHorasDiarias = useMemo(() => { + let totalMinutos = 0; + horarios.forEach(h => { + const [hIni, mIni] = h.hora_inicio.split(':').map(Number); + const [hFim, mFim] = h.hora_fim.split(':').map(Number); + if (!isNaN(hIni) && !isNaN(hFim)) { + const inicio = hIni * 60 + (mIni || 0); + const fim = hFim * 60 + (mFim || 0); + if (fim > inicio) totalMinutos += (fim - inicio); + } + }); + const h = Math.floor(totalMinutos / 60); + const m = totalMinutos % 60; + return m > 0 ? `${h}h${m}m` : `${h}h`; + }, [horarios]); + + // --- Funções de Dados --- + useEffect(() => { fetchDados(); }, []); + + const fetchDados = async () => { + try { + setLoading(true); + const [resEstagios, resAlunos, resEmpresas] = await Promise.all([ + supabase.from('estagios').select('*, alunos(nome, turma_curso), empresas(*)'), + supabase.from('alunos').select('id, nome, turma_curso').order('nome'), + supabase.from('empresas').select('*').order('nome') + ]); + if (resEstagios.data) setEstagios(resEstagios.data); + if (resAlunos.data) setAlunos(resAlunos.data); + if (resEmpresas.data) setEmpresas(resEmpresas.data); + } catch (e) { console.error(e); } finally { setLoading(false); } + }; + + const carregarHorarios = async (estagioId: string) => { + const { data } = await supabase.from('horarios_estagio').select('*').eq('estagio_id', estagioId); + if (data) setHorarios(data); + }; + + const handleFecharModal = () => { + setModalVisible(false); + setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); - setEditandoEstagio(null); - setModalVisible(true); + setHorarios([]); + setDataInicio(''); + setDataFim(''); + setPasso(1); }; - const salvarEstagio = () => { - if (!alunoSelecionado || !empresaSelecionada) { - Alert.alert('Erro', 'Selecione um aluno e uma empresa existentes.'); - return; + const eliminarEstagio = (id: string) => { + Alert.alert("Eliminar Estágio", "Deseja remover este estágio e todos os seus horários?", [ + { text: "Cancelar", style: "cancel" }, + { text: "Eliminar", style: "destructive", onPress: async () => { + await supabase.from('estagios').delete().eq('id', id); + fetchDados(); + }} + ]); + }; + + const salvarEstagio = async () => { + const { data: { user } } = await supabase.auth.getUser(); + + if (empresaSelecionada) { + await supabase.from('empresas').update({ + morada: empresaSelecionada.morada, + tutor_nome: empresaSelecionada.tutor_nome, + tutor_telefone: empresaSelecionada.tutor_telefone + }).eq('id', empresaSelecionada.id); } - if (editandoEstagio) { - // Editar estágio existente - setEstagios(estagios.map(e => e.id === editandoEstagio.id ? { - ...e, - alunoId: alunoSelecionado.id, - empresaId: empresaSelecionada.id, - alunoNome: alunoSelecionado.nome, - empresaNome: empresaSelecionada.nome - } : e)); - } else { - // Criar novo estágio - const novo: Estagio = { - id: Date.now(), - alunoId: alunoSelecionado.id, - empresaId: empresaSelecionada.id, - alunoNome: alunoSelecionado.nome, - empresaNome: empresaSelecionada.nome - }; - setEstagios([...estagios, novo]); + const payloadEstagio = { + aluno_id: alunoSelecionado?.id, + empresa_id: empresaSelecionada?.id, + professor_id: user?.id, + data_inicio: dataInicio || new Date().toISOString().split('T')[0], + data_fim: dataFim || null, + horas_diarias: totalHorasDiarias, + estado: 'Ativo', + }; + + const { data: estData, error: errE } = editandoEstagio + ? await supabase.from('estagios').update(payloadEstagio).eq('id', editandoEstagio.id).select().single() + : await supabase.from('estagios').insert([payloadEstagio]).select().single(); + + if (errE) return Alert.alert("Erro", errE.message); + + const currentId = editandoEstagio?.id || estData.id; + await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId); + + if (horarios.length > 0) { + const payloadH = horarios.map(h => ({ + estagio_id: currentId, + periodo: h.periodo, + hora_inicio: h.hora_inicio, + hora_fim: h.hora_fim + })); + await supabase.from('horarios_estagio').insert(payloadH); } - setModalVisible(false); + handleFecharModal(); + fetchDados(); }; - const editarEstagio = (e: Estagio) => { - setEditandoEstagio(e); - setAlunoSelecionado(alunosData.find(a => a.id === e.alunoId) || null); - setEmpresaSelecionada(empresasData.find(emp => emp.id === e.empresaId) || null); - setModalVisible(true); - }; + // --- Filtros --- + const alunosAgrupados = useMemo(() => { + const groups: Record = {}; + alunos.filter(a => a.nome.toLowerCase().includes(searchAluno.toLowerCase())).forEach(a => { + const k = a.turma_curso || 'Sem Turma'; + if (!groups[k]) groups[k] = []; + groups[k].push(a); + }); + return groups; + }, [alunos, searchAluno]); + + const empresasAgrupadas = useMemo(() => { + const groups: Record = {}; + empresas.filter(e => e.nome.toLowerCase().includes(searchEmpresa.toLowerCase())).forEach(e => { + const k = e.curso || 'Geral'; + if (!groups[k]) groups[k] = []; + groups[k].push(e); + }); + return groups; + }, [empresas, searchEmpresa]); + + if (loading) return ; return ( - - - - {/* Header */} - - router.back()}> - - - Estágios - - + + + + + router.back()}> + Estágios + + - {/* Pesquisa */} - + - {/* Lista de estágios */} - - {estagiosFiltrados.map(e => ( - editarEstagio(e)}> - {e.alunoNome} - {e.empresaNome} - - - ))} - {estagiosFiltrados.length === 0 && Nenhum estágio encontrado.} - - - {/* Botão Novo Estágio */} - - - Novo Estágio - - - {/* Modal de criação/edição */} - - - - {editandoEstagio ? 'Editar Estágio' : 'Novo Estágio'} - - {/* Dropdown Aluno */} - Aluno - {alunosData.map(a => ( - setAlunoSelecionado(a)} - > - {a.nome} - - ))} - - {/* Dropdown Empresa */} - Empresa - {empresasData.map(emp => ( - setEmpresaSelecionada(emp)} - > - {emp.nome} - - ))} - - - setModalVisible(false)} style={[styles.modalButton, { backgroundColor: cores.vermelho }]}> - Cancelar - - - Salvar - + {estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).map(e => ( + + { + setEditandoEstagio(e); + setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null); + setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null); + setDataInicio(e.data_inicio || ''); + setDataFim(e.data_fim || ''); + carregarHorarios(e.id); + setPasso(2); + setModalVisible(true); + }}> + {e.alunos?.nome} + + {e.alunos?.turma_curso} + ⏱ {e.horas_diarias || '0h'}/dia - + 🏢 {e.empresas?.nome} + + eliminarEstagio(e.id)}> - + ))} + + { + setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); + setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true); + }}> + + Novo Estágio + + + + + + + {passo === 1 ? ( + + Passo 1: Seleção + Aluno + + {Object.keys(alunosAgrupados).map(t => ( + {t} + {alunosAgrupados[t].map(a => ( + setAlunoSelecionado(a)}> + {a.nome} + + ))} + + ))} + + Empresa + + {Object.keys(empresasAgrupadas).map(c => ( + {c} + {empresasAgrupadas[c].map(emp => ( + setEmpresaSelecionada(emp)}> + {emp.nome} + + ))} + + ))} + + + Sair + { if(alunoSelecionado && empresaSelecionada) setPasso(2); }} style={[styles.btnModal, {backgroundColor: cores.azul}]}>Configurar + + + ) : ( + + Passo 2: Detalhes + + DURAÇÃO E HORAS + + + + + Total Diário: {totalHorasDiarias} + + + + + HORÁRIOS + setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}> + + {horarios.map((h, i) => ( + + { const n = [...horarios]; n[i].periodo = t; setHorarios(n); }} /> + { const n = [...horarios]; n[i].hora_inicio = t; setHorarios(n); }} /> + às + { const n = [...horarios]; n[i].hora_fim = t; setHorarios(n); }} /> + setHorarios(horarios.filter((_, idx) => idx !== i))}> + + ))} + + + + TUTOR + setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome"/> + setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/> + + + + editandoEstagio ? handleFecharModal() : setPasso(1)} + style={[styles.btnModal, {backgroundColor: cores.textoSecundario}]} + > + {editandoEstagio ? 'Cancelar' : 'Voltar'} + + Gravar + + + )} + + + + ); } -// Estilos const styles = StyleSheet.create({ - container: { flex: 1 }, - content: { padding: 24 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }, - title: { fontSize: 24, fontWeight: '700' }, - searchInput: { borderWidth: 1, borderRadius: 12, padding: 12, fontSize: 16, marginTop: 12 }, - card: { padding: 16, borderRadius: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, elevation: 2 }, - cardTitle: { fontSize: 16, fontWeight: '700' }, - cardSubtitle: { fontSize: 14 }, - newButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 16, borderRadius: 14, marginTop: 16 }, - newButtonText: { color: '#fff', fontWeight: '700', marginLeft: 8 }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', padding: 24 }, - modalContent: { borderRadius: 16, padding: 20 }, - modalTitle: { fontSize: 18, fontWeight: '700', marginBottom: 12 }, - modalLabel: { fontSize: 14, fontWeight: '600', marginBottom: 8 }, - modalOption: { padding: 12, borderRadius: 10, marginBottom: 6 }, - modalButton: { padding: 12, borderRadius: 12, flex: 1, alignItems: 'center', marginHorizontal: 4 } -}); + container: { + flex: 1, + paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 + }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 }, + title: { fontSize: 22, fontWeight: 'bold' }, + input: { borderWidth: 1, borderRadius: 12, padding: 12, marginBottom: 15 }, + card: { padding: 16, borderRadius: 15, flexDirection: 'row', alignItems: 'center', marginBottom: 10, elevation: 2 }, + cardTitle: { fontSize: 16, fontWeight: 'bold' }, + btnNovo: { padding: 18, borderRadius: 12, alignItems: 'center', marginTop: 10 }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 20 }, + modalContent: { borderRadius: 20, padding: 20, maxHeight: '90%' }, + modalTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 20, textAlign: 'center' }, + label: { fontSize: 14, fontWeight: 'bold', marginBottom: 5 }, + selector: { borderWidth: 1, borderColor: '#eee', borderRadius: 10, overflow: 'hidden' }, + groupHead: { fontSize: 11, backgroundColor: 'rgba(0,0,0,0.05)', padding: 6, fontWeight: 'bold', color: '#666' }, + item: { padding: 12, borderBottomWidth: 0.5, borderColor: '#eee' }, + modalFooter: { flexDirection: 'row', gap: 10, marginTop: 25 }, + btnModal: { flex: 1, padding: 15, borderRadius: 12, alignItems: 'center' }, + confirmBox: { borderWidth: 1, borderRadius: 12, padding: 15, backgroundColor: 'rgba(0,0,0,0.02)' }, + confirmTitle: { fontSize: 10, fontWeight: '900', color: '#0d6efd', marginBottom: 8, letterSpacing: 1 }, + editInput: { borderBottomWidth: 1, paddingVertical: 5, fontSize: 14, borderColor: '#eee' }, + horarioRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 10 }, + miniInput: { borderBottomWidth: 1, borderColor: '#eee', flex: 1, padding: 4, fontSize: 12, textAlign: 'center' }, + badgeHoras: { backgroundColor: '#0d6efd', padding: 5, borderRadius: 6, alignSelf: 'flex-start' }, + badgeText: { color: '#fff', fontSize: 11, fontWeight: 'bold' } +}); \ No newline at end of file diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index a66d6ef..4d249b1 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -1,9 +1,11 @@ import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { + ActivityIndicator, Alert, - Platform, + Platform // Importado para detetar o sistema operativo + , SafeAreaView, ScrollView, StatusBar, @@ -11,147 +13,190 @@ import { Text, TextInput, TouchableOpacity, - View, + View } from 'react-native'; -import { useTheme } from '../../themecontext'; // Contexto global do tema +import { useTheme } from '../../themecontext'; import { supabase } from '../lib/supabase'; +interface PerfilData { + id: string; + nome: string; + email: string; + n_escola: string; + telefone: string; + residencia: string; + tipo: string; + area: string; + idade?: number; +} + export default function PerfilProfessor() { const router = useRouter(); - const { isDarkMode } = useTheme(); // pega do contexto global + const { isDarkMode } = useTheme(); const [editando, setEditando] = useState(false); + const [loading, setLoading] = useState(true); + const [perfil, setPerfil] = useState(null); - // Cores dinamicas baseadas no tema const cores = { fundo: isDarkMode ? '#121212' : '#f1f3f5', card: isDarkMode ? '#1e1e1e' : '#fff', texto: isDarkMode ? '#fff' : '#212529', textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - inputBg: isDarkMode ? '#1e1e1e' : '#f8f9fa', + inputBg: isDarkMode ? '#2c2c2c' : '#f8f9fa', border: isDarkMode ? '#343a40' : '#ced4da', azul: '#0d6efd', vermelho: '#dc3545', }; - const [perfil, setPerfil] = useState({ - nome: 'João Miranda', - email: 'joao.miranda@epvc.pt', - mecanografico: 'PROF-174', - area: 'Informática', - turmas: '12ºINF | 11ºINF | 10ºINF', - funcao: 'Orientador de Estágio', - }); + useEffect(() => { + carregarPerfil(); + }, []); + + 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); + } + } catch (error: any) { + Alert.alert('Erro', 'Não foi possível carregar os dados do perfil.'); + } finally { + setLoading(false); + } + } + + const guardarPerfil = async () => { + if (!perfil) return; + try { + const { error } = await supabase + .from('profiles') + .update({ + nome: perfil.nome, + telefone: perfil.telefone, + residencia: perfil.residencia, + n_escola: perfil.n_escola, + area: perfil.area + }) + .eq('id', perfil.id); + if (error) throw error; + setEditando(false); + Alert.alert('Sucesso', 'Perfil atualizado!'); + } catch (error: any) { + Alert.alert('Erro', error.message); + } + }; const terminarSessao = async () => { await supabase.auth.signOut(); router.replace('/'); }; - const guardarPerfil = () => { - setEditando(false); - Alert.alert('Sucesso', 'Perfil atualizado com sucesso!'); - // depois liga ao Supabase update - }; + if (loading) { + return ( + + + + ); + } return ( - - - - {/* TOPO COM VOLTAR */} + // AJUSTE DE SAFE AREA AQUI: paddingTop dinâmico para Android e iOS + + + + + {/* TOPO COM ESPAÇAMENTO PARA NOTIFICAÇÕES */} - router.push('/Professor/ProfessorHome')}> + router.back()}> - Perfil + O Meu Perfil - {/* HEADER */} + {/* HEADER PERFIL */} - {perfil.nome} - {perfil.funcao} + {perfil?.nome} + + {perfil?.area || 'Professor Orientador'} + - {/* INFORMAÇÕES */} + {/* CAMPOS DE INFORMAÇÃO */} setPerfil({ ...perfil, nome: v })} - cores={cores} - /> - - setPerfil({ ...perfil, mecanografico: v })} + onChange={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} /> setPerfil({ ...perfil, area: v })} + onChange={(v: string) => setPerfil(prev => prev ? { ...prev, area: v } : null)} + cores={cores} + /> + + setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} /> setPerfil({ ...perfil, turmas: v })} + onChange={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)} cores={cores} /> - {/* AÇÕES */} + {/* BOTÕES DE AÇÃO */} {editando ? ( - Guardar alterações + Guardar Alterações ) : ( - setEditando(true)} cores={cores} /> + setEditando(true)}> + + Editar Perfil + )} - router.push('/Professor/redefenirsenha2')} - cores={cores} - /> - - + + + Terminar Sessão + ); } -/* COMPONENTES */ -function InfoField({ - label, - value, - editable, - onChange, - cores, -}: { - label: string; - value: string; - editable: boolean; - onChange?: (v: string) => void; - cores: any; -}) { +function InfoField({ label, value, editable, onChange, cores }: any) { return ( {label} @@ -162,71 +207,41 @@ function InfoField({ style={[styles.input, { backgroundColor: cores.inputBg, color: cores.texto, borderColor: cores.border }]} /> ) : ( - {value} + {value || 'Não definido'} )} ); } -function ActionButton({ - icon, - text, - onPress, - danger = false, - cores, -}: { - icon: any; - text: string; - onPress: () => void; - danger?: boolean; - cores: any; -}) { - return ( - - - - {text} - - - ); -} - -/* ESTILOS */ const styles = StyleSheet.create({ - container: { flex: 1 }, - content: { padding: 24 }, - - topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }, + // Estilo principal da Safe Area + safeArea: { + flex: 1, + // No Android, a barra de notificações não é respeitada automaticamente pelo SafeAreaView + paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 + }, + centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + content: { padding: 24, paddingBottom: 40 }, + topBar: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 20, + marginTop: 10 // Margem extra de segurança + }, topTitle: { fontSize: 18, fontWeight: '700' }, - header: { alignItems: 'center', marginBottom: 32 }, avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', marginBottom: 12 }, name: { fontSize: 22, fontWeight: '800' }, role: { fontSize: 14, marginTop: 4 }, - card: { borderRadius: 18, padding: 20, marginBottom: 24, elevation: 4 }, - infoField: { marginBottom: 16 }, - label: { fontSize: 12, marginBottom: 4 }, - value: { fontSize: 15, fontWeight: '600' }, - input: { borderWidth: 1, borderRadius: 10, padding: 10, fontSize: 15 }, - + label: { fontSize: 11, marginBottom: 4, textTransform: 'uppercase', fontWeight: '600' }, + value: { fontSize: 16, fontWeight: '600' }, + input: { borderWidth: 1, borderRadius: 10, padding: 12, fontSize: 15 }, actions: { gap: 12 }, actionButton: { flexDirection: 'row', alignItems: 'center', borderRadius: 14, padding: 16 }, actionText: { fontSize: 15, fontWeight: '600', marginLeft: 12 }, primaryButton: { borderRadius: 14, padding: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }, primaryText: { color: '#fff', fontWeight: '700', marginLeft: 8 }, -}); +}); \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 0e17fda..57dca80 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -16,51 +16,44 @@ export default function LoginScreen() { const handleLoginSuccess = async () => { try { - // 1️⃣ Obter utilizador autenticado + // Buscar utilizador autenticado const { data: { user }, error: userError, } = await supabase.auth.getUser(); if (userError || !user) { - console.log('Erro ao obter utilizador:', userError); Alert.alert('Erro', 'Utilizador não autenticado'); return; } - console.log('Utilizador autenticado:', user.id); - - // 2️⃣ Buscar tipo do utilizador + // Buscar perfil const { data, error } = await supabase .from('profiles') .select('tipo') .eq('id', user.id) - .single(); + .maybeSingle(); if (error) { - console.log('Erro ao buscar perfil:', error); Alert.alert('Erro', error.message); return; } if (!data) { - console.log('Perfil não encontrado'); Alert.alert('Erro', 'Perfil não encontrado'); return; } - console.log('Tipo de utilizador:', data.tipo); - - // 3️⃣ Redirecionar conforme o tipo + // Redirecionar if (data.tipo === 'professor') { router.replace('/Professor/ProfessorHome'); } else if (data.tipo === 'aluno') { router.replace('/Aluno/AlunoHome'); } else { - Alert.alert('Erro', 'Tipo de utilizador inválido'); + Alert.alert('Erro', 'Tipo inválido'); } + } catch (err) { - console.error('Erro inesperado:', err); Alert.alert('Erro', 'Erro inesperado no login'); } }; @@ -82,7 +75,6 @@ export default function LoginScreen() { - {/* COMPONENTE DE LOGIN */} @@ -116,4 +108,4 @@ const styles = StyleSheet.create({ color: '#636e72', textAlign: 'center', }, -}); +}); \ No newline at end of file diff --git a/components/Auth.tsx b/components/Auth.tsx index af6de07..e774286 100644 --- a/components/Auth.tsx +++ b/components/Auth.tsx @@ -1,4 +1,4 @@ -import { useRouter } from 'expo-router'; // IMPORTAR ROUTER +import { useRouter } from 'expo-router'; import { useState } from 'react'; import { ActivityIndicator, Alert, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { supabase } from '../app/lib/supabase'; @@ -11,9 +11,8 @@ export default function Auth({ onLoginSuccess }: AuthProps) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); - const router = useRouter(); // INICIALIZA O ROUTER + const router = useRouter(); - // LOGIN const handleLogin = async () => { if (!email || !password) { Alert.alert('Atenção', 'Preencha todos os campos'); @@ -22,11 +21,34 @@ export default function Auth({ onLoginSuccess }: AuthProps) { setLoading(true); try { - const { error } = await supabase.auth.signInWithPassword({ email, password }); - if (error) throw error; + // 1. LOGIN NO AUTH DO SUPABASE + const { data: { user }, error: authError } = await supabase.auth.signInWithPassword({ email, password }); + if (authError) throw authError; + + // 2. BUSCAR DADOS NA TABELA PROFILES LOGO APÓS O LOGIN + if (user) { + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', user.id) + .single(); + + if (profileError) { + console.warn("Perfil não encontrado na tabela profiles."); + } else { + console.log("Perfil carregado com sucesso:", profile.nome); + } + } Alert.alert('Bem-vindo(a)!'); - if (onLoginSuccess) onLoginSuccess(); + + // 3. SE SUCESSO, EXECUTA O CALLBACK E NAVEGA + if (onLoginSuccess) { + onLoginSuccess(); + } else { + router.replace('/(tabs)/estagios'); // Caminho padrão caso não venha callback + } + } catch (err: any) { Alert.alert('Erro', err.message); } finally { @@ -36,7 +58,6 @@ export default function Auth({ onLoginSuccess }: AuthProps) { return ( - {/* EMAIL */} Email - {/* PASSWORD */} Palavra-passe - {/* BOTÃO ENTRAR MODERNO */} - {loading ? ( - - ) : ( - ENTRAR - )} + {loading ? : ENTRAR} - {/* TEXTO DE ESQUECI A SENHA → NAVEGA */} router.push('/redefenirsenha')}> Esqueceu a palavra-passe? @@ -80,22 +94,13 @@ export default function Auth({ onLoginSuccess }: AuthProps) { ); } +// ... (teus estilos mantêm-se iguais) const styles = StyleSheet.create({ - form: { - backgroundColor: '#fff', - borderRadius: 16, - padding: 24, - marginTop: 20, - shadowColor: '#000', - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.1, - shadowRadius: 12, - elevation: 5, - }, + form: { backgroundColor: '#fff', borderRadius: 16, padding: 24, marginTop: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 5 }, label: { fontSize: 14, fontWeight: '600', color: '#2d3436', marginBottom: 8 }, input: { backgroundColor: '#f1f2f6', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, marginBottom: 20, borderWidth: 0, color: '#2d3436' }, button: { backgroundColor: '#0984e3', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginBottom: 12, shadowColor: '#0984e3', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 3 }, buttonDisabled: { backgroundColor: '#74b9ff' }, buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' }, forgotText: { color: '#0984e3', fontSize: 15, fontWeight: '500', textAlign: 'center', marginTop: 8 }, -}); +}); \ No newline at end of file