criar aluno

This commit is contained in:
2026-04-15 12:48:46 +01:00
parent 83095f5a2d
commit 82aae4d3de
2 changed files with 327 additions and 77 deletions

View File

@@ -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 (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { borderColor: cores.borda }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
<View>
<Text style={[styles.title, { color: cores.texto }]}>Novo Registo</Text>
<Text style={[styles.subtitle, { color: cores.laranja }]}>Criar utilizador no sistema</Text>
</View>
</View>
{/* SELETOR DE TIPO */}
<Text style={styles.sectionTitle}>Tipo de Utilizador</Text>
<View style={styles.selectorContainer}>
{(['aluno', 'professor', 'empresa'] as const).map((item) => (
<TouchableOpacity
key={item}
style={[
styles.selectorBtn,
{ backgroundColor: tipo === item ? cores.azul : cores.card, borderColor: cores.borda }
]}
onPress={() => setTipo(item)}
>
<Text style={[styles.selectorText, { color: tipo === item ? '#FFF' : cores.secundario }]}>
{item.charAt(0).toUpperCase() + item.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.form}>
<Text style={styles.sectionTitle}>Dados de Acesso</Text>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={email} onChangeText={setEmail} placeholder="Email" autoCapitalize="none" placeholderTextColor={cores.secundario}
/>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={password} onChangeText={setPassword} secureTextEntry placeholder="Password (mín. 6 caracteres)" placeholderTextColor={cores.secundario}
/>
<Text style={styles.sectionTitle}>Informação Geral</Text>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? "Nome da Empresa" : "Nome Completo"} placeholderTextColor={cores.secundario}
/>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={residencia} onChangeText={setResidencia} placeholder="Morada" placeholderTextColor={cores.secundario}
/>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={telefone} onChangeText={setTelefone} keyboardType="phone-pad" placeholder="Telemóvel" placeholderTextColor={cores.secundario}
/>
{/* CAMPOS ESPECÍFICOS PARA ALUNO */}
{tipo === 'aluno' && (
<>
<Text style={styles.sectionTitle}>Dados Escolares</Text>
<View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={ano} onChangeText={setAno} keyboardType="numeric" placeholder="Ano" placeholderTextColor={cores.secundario}
/>
<TextInput
style={[styles.input, { flex: 2, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={nEscola} onChangeText={setNEscola} keyboardType="numeric" placeholder="Nº Aluno" placeholderTextColor={cores.secundario}
/>
</View>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={curso} onChangeText={setCurso} autoCapitalize="characters" placeholder="Curso" placeholderTextColor={cores.secundario}
/>
</>
)}
</View>
<TouchableOpacity
style={[styles.submitBtn, { backgroundColor: cores.azul }]}
onPress={handleCriar}
disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.submitBtnText}>REGISTAR {tipo.toUpperCase()}</Text>}
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
};
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;

View File

@@ -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<Aluno | null>(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<string, Aluno[]> = {};
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 (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER EPVC STYLE */}
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Alunos</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Gestão de Turmas</Text>
</View>
<TouchableOpacity
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={fetchAlunos}
>
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={fetchAlunos}>
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
</View>
{/* SEARCH MODERNO */}
{/* SEARCH */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar por aluno ou nº..."
placeholder="Pesquisar por aluno..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
@@ -177,59 +181,73 @@ const ListaAlunosProfessor = memo(() => {
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
{item.alunos.map((aluno) => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.8}
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => 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);
}}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>
{aluno.nome.charAt(0)}
</Text>
<Text style={[styles.avatarText, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<View style={styles.idRow}>
<Ionicons name="finger-print-outline" size={13} color={cores.secundario} />
<Text style={[styles.idText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
<Text style={[styles.idText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
{aluno.tem_estagio ? (
<View style={[styles.statusBadge, { backgroundColor: cores.verde + '20' }]}>
<Ionicons name="checkmark-circle" size={12} color={cores.verde} />
<Text style={[styles.statusText, { color: cores.verde }]}>COLOCADO</Text>
</View>
) : (
<View style={[styles.statusBadge, { backgroundColor: cores.laranja + '20' }]}>
<Ionicons name="alert-circle" size={12} color={cores.laranja} />
<Text style={[styles.statusText, { color: cores.laranja }]}>PENDENTE</Text>
</View>
)}
<Ionicons name="ellipsis-vertical" size={18} color={cores.borda} />
</TouchableOpacity>
))}
</View>
)}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="people-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Nenhum aluno encontrado.</Text>
</View>
)}
/>
)}
{/* FAB */}
{/* MODAL DE ELIMINAÇÃO CUSTOMIZADO */}
<Modal visible={showDeleteModal} transparent animationType="fade">
<View style={styles.modalOverlay}>
<View style={[styles.modalCard, { backgroundColor: cores.fundo }]}>
<View style={[styles.warningIconBox, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="trash-outline" size={32} color={cores.vermelho} />
</View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Aluno?</Text>
<Text style={[styles.modalDesc, { color: cores.secundario }]}>
Estás prestes a apagar <Text style={{fontWeight:'800', color: cores.texto}}>{alunoParaEliminar?.nome}</Text>.
Esta ação é irreversível e **vai dar merda** se não tiveres a certeza!
</Text>
<View style={styles.modalButtons}>
<TouchableOpacity
style={[styles.btnModal, { borderColor: cores.borda, borderWidth: 1 }]}
onPress={() => setShowDeleteModal(false)}
>
<Text style={[styles.btnText, { color: cores.secundario }]}>Cancelar</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btnModal, { backgroundColor: cores.vermelho }]}
onPress={confirmarEliminacao}
disabled={isDeleting}
>
{isDeleting ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={[styles.btnText, { color: '#fff' }]}>Eliminar</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<TouchableOpacity
style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]}
onPress={() => Alert.alert("EPVC", "Funcionalidade de registo de aluno em desenvolvimento.")}
onPress={() => router.push('/Professor/Alunos/CriarAluno')} // Altera esta linha
>
<Ionicons name="person-add" size={24} color="#fff" />
<Text style={styles.fabText}>Novo Aluno</Text>
@@ -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;