From 82aae4d3dedd9a8b12ae8f487d61fd61fecab250 Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Wed, 15 Apr 2026 12:48:46 +0100 Subject: [PATCH] criar aluno --- app/Professor/Alunos/CriarAluno.tsx | 227 +++++++++++++++++++++++++++ app/Professor/Alunos/ListaAlunos.tsx | 177 ++++++++++++--------- 2 files changed, 327 insertions(+), 77 deletions(-) create mode 100644 app/Professor/Alunos/CriarAluno.tsx diff --git a/app/Professor/Alunos/CriarAluno.tsx b/app/Professor/Alunos/CriarAluno.tsx new file mode 100644 index 0000000..074c802 --- /dev/null +++ b/app/Professor/Alunos/CriarAluno.tsx @@ -0,0 +1,227 @@ +// app/Professor/Alunos/CriarAluno.tsx +import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; +import { useMemo, useState } from 'react'; +import { + ActivityIndicator, + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTheme } from '../../../themecontext'; +import { supabase } from '../../lib/supabase'; + +const CriarAluno = () => { + const { isDarkMode } = useTheme(); + const router = useRouter(); + const [loading, setLoading] = useState(false); + + // ESTADOS + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [nome, setNome] = useState(''); + const [residencia, setResidencia] = useState(''); + const [telefone, setTelefone] = useState(''); + const [ano, setAno] = useState(''); + const [nEscola, setNEscola] = useState(''); + const [curso, setCurso] = useState(''); + const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno'); + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: '#2390a6', + laranja: '#E38E00', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + }), [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."); + return; + } + + setLoading(true); + + try { + // 1. Criar Utilizador no Auth + const { data: authData, error: authError } = await supabase.auth.signUp({ + email, + password, + options: { data: { nome, tipo } } + }); + + if (authError) throw authError; + const user = authData.user; + if (!user) throw new Error("Erro ao gerar ID."); + + // 2. Tabela 'profiles' + const { error: profileError } = await supabase + .from('profiles') + .upsert({ + id: user.id, + nome, + email, + residencia, + telefone, + n_escola: tipo === 'aluno' ? nEscola : null, + curso: tipo === 'aluno' ? curso.toUpperCase() : null, + tipo + }); + + if (profileError) throw profileError; + + // 3. Tabela 'alunos' (SÓ SE FOR ALUNO) + if (tipo === 'aluno') { + const { error: alunoError } = await supabase + .from('alunos') + .insert([{ + id: user.id, + nome, + n_escola: nEscola, + ano: parseInt(ano), + turma_curso: curso.toUpperCase() + }]); + if (alunoError) throw alunoError; + } + + Alert.alert("Sucesso", `${tipo.toUpperCase()} criado com sucesso!`, [ + { text: "OK", onPress: () => router.back() } + ]); + + } catch (err: any) { + console.error(err); + Alert.alert("Erro no Registo", err.message); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + + 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)} + + + ))} + + + + Dados de Acesso + + + + Informação Geral + + + + + {/* CAMPOS ESPECÍFICOS PARA ALUNO */} + {tipo === 'aluno' && ( + <> + Dados Escolares + + + + + + + )} + + + + {loading ? : REGISTAR {tipo.toUpperCase()}} + + + + + + + ); +}; + +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 }, + 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' }, +}); + +export default CriarAluno; \ No newline at end of file diff --git a/app/Professor/Alunos/ListaAlunos.tsx b/app/Professor/Alunos/ListaAlunos.tsx index 6bb58ed..2a01e13 100644 --- a/app/Professor/Alunos/ListaAlunos.tsx +++ b/app/Professor/Alunos/ListaAlunos.tsx @@ -6,6 +6,7 @@ import { ActivityIndicator, Alert, FlatList, + Modal, RefreshControl, StatusBar, StyleSheet, @@ -42,6 +43,11 @@ const ListaAlunosProfessor = memo(() => { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); + // Estados para o Modal de Eliminação + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [alunoParaEliminar, setAlunoParaEliminar] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; @@ -55,6 +61,8 @@ const ListaAlunosProfessor = memo(() => { 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]); const fetchAlunos = async () => { @@ -62,22 +70,14 @@ 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, estagios(id)`) .order('ano', { ascending: false }) .order('nome', { ascending: true }); if (error) throw error; - if (!data) return setTurmas([]); - + const agrupadas: Record = {}; - data.forEach(item => { + data?.forEach(item => { const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase(); if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = []; agrupadas[nomeTurma].push({ @@ -89,18 +89,32 @@ const ListaAlunosProfessor = memo(() => { }); }); - 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('Erro:', err); + console.error(err); } finally { setLoading(false); setRefreshing(false); } }; + const confirmarEliminacao = async () => { + if (!alunoParaEliminar) return; + try { + setIsDeleting(true); + const { error } = await supabase.from('alunos').delete().eq('id', alunoParaEliminar.id); + if (error) throw error; + + setShowDeleteModal(false); + setAlunoParaEliminar(null); + fetchAlunos(); + } catch (err) { + Alert.alert("Erro", "Não foi possível eliminar o aluno."); + } finally { + setIsDeleting(false); + } + }; + useEffect(() => { fetchAlunos(); }, []); const onRefresh = useCallback(() => { @@ -113,8 +127,7 @@ const ListaAlunosProfessor = memo(() => { .map(turma => ({ ...turma, alunos: turma.alunos.filter(a => - a.nome.toLowerCase().includes(search.toLowerCase()) || - a.n_escola.includes(search) + a.nome.toLowerCase().includes(search.toLowerCase()) || a.n_escola.includes(search) ), })) .filter(t => t.alunos.length > 0); @@ -123,38 +136,29 @@ const ListaAlunosProfessor = memo(() => { return ( - - {/* HEADER EPVC STYLE */} + {/* HEADER */} - router.back()} - > + router.back()}> - Alunos Gestão de Turmas - - + - {/* SEARCH MODERNO */} + {/* SEARCH */} { {item.nome} - {item.alunos.map((aluno) => ( router.push({ - pathname: '/Professor/Alunos/DetalhesAluno', - params: { alunoId: aluno.id } - })} + onPress={() => router.push({ pathname: '/Professor/Alunos/DetalhesAluno', params: { alunoId: aluno.id } })} + onLongPress={() => { + setAlunoParaEliminar(aluno); + setShowDeleteModal(true); + }} > - - {aluno.nome.charAt(0)} - + {aluno.nome.charAt(0).toUpperCase()} - {aluno.nome} - - - Nº {aluno.n_escola} - + Nº {aluno.n_escola} - - {aluno.tem_estagio ? ( - - - COLOCADO - - ) : ( - - - PENDENTE - - )} + ))} )} - ListEmptyComponent={() => ( - - - Nenhum aluno encontrado. - - )} /> )} - {/* FAB */} + {/* MODAL DE ELIMINAÇÃO CUSTOMIZADO */} + + + + + + + + Eliminar Aluno? + + Estás prestes a apagar {alunoParaEliminar?.nome}. + Esta ação é irreversível e **vai dar merda** se não tiveres a certeza! + + + + setShowDeleteModal(false)} + > + Cancelar + + + + {isDeleting ? ( + + ) : ( + Eliminar + )} + + + + + + Alert.alert("EPVC", "Funcionalidade de registo de aluno em desenvolvimento.")} + onPress={() => router.push('/Professor/Alunos/CriarAluno')} // Altera esta linha > Novo Aluno @@ -242,29 +260,34 @@ const ListaAlunosProfessor = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + headerTitle: { fontSize: 22, fontWeight: '900' }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, searchSection: { paddingHorizontal: 24, marginBottom: 10 }, searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, listPadding: { paddingHorizontal: 24, paddingTop: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase' }, sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1 }, avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, avatarText: { fontSize: 18, fontWeight: '900' }, alunoInfo: { flex: 1, marginLeft: 15 }, - alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, - idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, + alunoNome: { fontSize: 16, fontWeight: '800' }, idText: { fontSize: 13, fontWeight: '600' }, - statusBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10 }, - statusText: { fontSize: 9, fontWeight: '900' }, - emptyContainer: { marginTop: 80, alignItems: 'center' }, - fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 }, - fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase' }, + fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22 }, + 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 }, + 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' }, + btnText: { fontSize: 16, fontWeight: '800' } }); export default ListaAlunosProfessor; \ No newline at end of file