criar/dados aluno

This commit is contained in:
2026-04-21 17:19:35 +01:00
parent 82aae4d3de
commit bba6d2de08
6 changed files with 596 additions and 386 deletions

View File

@@ -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(() => {
<View style={[styles.iconCircle, { backgroundColor: azulPetroleo + '15' }]}>
<Ionicons name="location" size={40} color={azulPetroleo} />
</View>
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Validar Localização</Text>
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Confirmar Local</Text>
<Text style={[styles.modalDesc, { color: themeStyles.textoSecundario }]}>
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.
</Text>
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: azulPetroleo }]} onPress={executarMarcacao}>
<Text style={styles.txtBtn}>Confirmar e Marcar</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnFechar} onPress={() => setShowLocationModal(false)}>
<Text style={[styles.txtFechar, { color: themeStyles.textoSecundario }]}>Agora não</Text>
<Text style={[styles.txtFechar, { color: themeStyles.textoSecundario }]}>Cancelar</Text>
</TouchableOpacity>
</View>
</View>
@@ -237,16 +260,34 @@ const AlunoHome = memo(() => {
</View>
</View>
{/* AVISO DE FALTA DE ESTÁGIO - Vai dar merda se o aluno não souber por que está bloqueado */}
{!infoData.temEstagio && !isLoadingDB && (
<View style={[styles.avisoBox, { backgroundColor: themeStyles.aviso }]}>
<Ionicons name="alert-circle" size={20} color={themeStyles.avisoTexto} />
<Text style={[styles.avisoTexto, { color: themeStyles.avisoTexto }]}>
O teu período de estágio ainda não foi configurado pelo professor.
</Text>
</View>
)}
<View style={styles.botoesLinha}>
<TouchableOpacity
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
style={[
styles.btn,
{ backgroundColor: laranjaEPVC },
(!infoData.podeMarcar || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled
]}
onPress={handlePresencaClick}
disabled={!infoData.podeMarcar || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
>
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, { backgroundColor: themeStyles.vermelho }, (!infoData.valida || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
style={[
styles.btn,
{ backgroundColor: themeStyles.vermelho },
(!infoData.valida || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled
]}
onPress={handleFalta}
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
>
@@ -280,7 +321,7 @@ const AlunoHome = memo(() => {
{presencas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.rowTitle}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário do Dia</Text>
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>
</View>
<TextInput
@@ -288,7 +329,7 @@ const AlunoHome = memo(() => {
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 && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar Sumário</Text></TouchableOpacity>}
@@ -297,11 +338,11 @@ const AlunoHome = memo(() => {
{faltas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar Falta</Text>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar</Text>
{urlsJustificacao[selectedDate] ? (
<View style={styles.justificadoBox}>
<Ionicons name="checkmark-circle" size={20} color={themeStyles.verde} />
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Documento Enviado</Text>
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Justificativo Enviado</Text>
</View>
) : (
<>
@@ -311,7 +352,7 @@ const AlunoHome = memo(() => {
</TouchableOpacity>
{pdf && (
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: azulPetroleo }]} onPress={enviarJustificativo} disabled={isUploading}>
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Justificativo</Text>}
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter</Text>}
</TouchableOpacity>
)}
</>
@@ -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' },

View File

@@ -19,9 +19,6 @@ export default function PerfilAluno() {
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [perfil, setPerfil] = useState<any>(null);
const [estagio, setEstagio] = useState<any>(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 <View style={[styles.centered, { backgroundColor: cores.fundo }]}><ActivityIndicator size="large" color={cores.azul} /></View>;
return (
@@ -171,7 +133,7 @@ export default function PerfilAluno() {
</Animated.View>
)}
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
<SafeAreaView style={styles.safe} edges={['top']}>
<View style={styles.topBar}>
<TouchableOpacity style={[styles.backBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
@@ -194,53 +156,51 @@ export default function PerfilAluno() {
</View>
</View>
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.userRole, { color: cores.secundario }]}> Aluno: {perfil?.n_escola || '---'}</Text>
<Text style={[styles.userRole, { color: cores.secundario }]}>{perfil?.curso || 'Sem Curso'} {perfil?.n_escola || '---'}</Text>
</View>
{/* PROGRESS CARD */}
<View style={[styles.card, { backgroundColor: cores.card, marginBottom: 25 }]}>
<Text style={[styles.inputLabel, { color: cores.secundario, marginBottom: 15 }]}>Progresso do Estágio</Text>
<View style={styles.statsGrid}>
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cores.azul }]}>{horasRealizadas}h</Text>
<Text style={[styles.statLabel, { color: cores.secundario }]}>Feitas</Text>
</View>
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cores.laranja }]}>{Math.max(0, horasTotais - horasRealizadas)}h</Text>
<Text style={[styles.statLabel, { color: cores.secundario }]}>Restam</Text>
</View>
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cores.vermelho }]}>{contagemFaltas}</Text>
<Text style={[styles.statLabel, { color: cores.secundario }]}>Faltas</Text>
</View>
{/* DADOS ACADÉMICOS */}
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Informação Académica</Text>
<View style={[styles.card, { backgroundColor: cores.card, marginBottom: 20 }]}>
<View style={styles.row}>
<View style={{ flex: 1, marginRight: 10 }}>
<ModernInput
label="Nº Aluno" icon="card-outline"
value={perfil?.n_escola || ''}
editable={isEditing}
onChangeText={(v: string) => setPerfil({...perfil, n_escola: v})}
cores={cores} keyboardType="numeric"
/>
</View>
<View style={{ flex: 1 }}>
<ModernInput
label="Curso (Sigla)" icon="school-outline"
value={perfil?.curso || ''}
editable={isEditing}
onChangeText={(v: string) => setPerfil({...perfil, curso: v})}
cores={cores} autoCapitalize="characters"
/>
</View>
</View>
<View style={[styles.progressBarBase, { backgroundColor: cores.fundo, marginTop: 15 }]}>
<View style={[styles.progressBarFill, { width: `${progresso * 100}%`, backgroundColor: cores.azul }]} />
</View>
<Text style={[styles.progressText, { color: cores.secundario }]}>{Math.round(progresso * 100)}% das {horasTotais}h úteis concluídas</Text>
<ModernInput label="E-mail Institucional" icon="mail-outline" value={perfil?.email || ''} editable={false} cores={cores} />
</View>
{/* INFO CARD */}
{/* DADOS PESSOAIS */}
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Pessoais</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<ModernInput label="Nome Completo" icon="person-outline" value={perfil?.nome || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, nome: v})} cores={cores} />
<ModernInput label="E-mail Institucional" icon="mail-outline" value={perfil?.email || ''} editable={false} cores={cores} />
<View style={styles.row}>
{/* Aumentado o flex da data para 1.2 para evitar corte */}
<View style={{ flex: 1.2, marginRight: 10 }}>
<View style={{ flex: 1, marginRight: 10 }}>
<ModernInput
label="Nascimento"
icon="calendar-outline"
label="Nascimento" icon="calendar-outline"
value={perfil?.data_nascimento || ''}
editable={isEditing}
onChangeText={(v: string) => setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})}
cores={cores}
maxLength={10}
keyboardType="numeric"
placeholder="DD-MM-AAAA"
cores={cores} maxLength={10} keyboardType="numeric"
/>
</View>
<View style={{ flex: 1.3 }}>
<View style={{ flex: 1.2 }}>
<ModernInput label="Telefone" icon="call-outline" value={perfil?.telefone || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, telefone: v})} keyboardType="phone-pad" cores={cores} />
</View>
</View>
@@ -248,31 +208,8 @@ export default function PerfilAluno() {
<ModernInput label="Residência" icon="location-outline" value={perfil?.residencia || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, residencia: v})} cores={cores} />
</View>
{/* ESTÁGIO CARD ATUALIZADO */}
<View style={[styles.card, { backgroundColor: cores.card, marginTop: 20 }]}>
<Text style={[styles.inputLabel, { color: cores.secundario, marginBottom: 15 }]}>Informações do Estágio</Text>
<View style={styles.row}>
<View style={{ flex: 1 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Início</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{formatarParaExibir(estagio?.data_inicio)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Fim</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{formatarParaExibir(estagio?.data_fim)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Horário</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{estagio?.horario || '09:00-17:00'}</Text>
</View>
</View>
<View style={{ marginTop: 15 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Empresa e Tutor</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{estagio?.empresas?.nome}</Text>
<Text style={{ color: cores.secundario, fontSize: 13 }}>{estagio?.empresas?.tutor_nome}</Text>
</View>
</View>
<View style={[styles.actionsContainer, { marginTop: 25 }]}>
{/* ACÇÕES */}
<View style={[styles.actionsContainer, { marginTop: 30 }]}>
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card }]} onPress={() => router.push('/Aluno/redefenirsenha')}>
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
@@ -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 },

View File

@@ -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 = () => {
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { borderColor: cores.borda }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Novo Registo</Text>
</View>
<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 }
]}
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 style={{ color: tipo === item ? '#FFF' : cores.secundario, fontWeight: '900', fontSize: 12 }}>
{item.toUpperCase()}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.form}>
<Text style={styles.sectionTitle}>Dados de Acesso</Text>
<SectionHeader title="Credenciais de Acesso" cores={cores} />
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={email} onChangeText={setEmail} placeholder="Email" autoCapitalize="none" placeholderTextColor={cores.secundario}
value={email} onChangeText={setEmail} placeholder="Email Institucional" autoCapitalize="none" placeholderTextColor={cores.placeholder}
/>
<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}
value={password} onChangeText={setPassword} secureTextEntry placeholder="Password (mín. 6 caracteres)" placeholderTextColor={cores.placeholder}
/>
<Text style={styles.sectionTitle}>Informação Geral</Text>
<SectionHeader title="Dados Pessoais" cores={cores} />
<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}
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? "Nome da Entidade" : "Nome Completo"} placeholderTextColor={cores.placeholder}
/>
<View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput
style={[styles.input, { flex: 2, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={dataNascimento} onChangeText={setDataNascimento} placeholder="Nascimento (AAAA-MM-DD)" placeholderTextColor={cores.placeholder}
/>
<View style={[styles.input, { flex: 1, backgroundColor: cores.card, borderColor: cores.borda, justifyContent: 'center', opacity: 0.7 }]}>
<Text style={{ color: cores.texto, textAlign: 'center' }}>{idade ? `${idade} anos` : 'Idade'}</Text>
</View>
</View>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={telefone} onChangeText={setTelefone} keyboardType="phone-pad" placeholder="Contacto Telefónico" placeholderTextColor={cores.placeholder}
/>
<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}
value={residencia} onChangeText={setResidencia} placeholder="Morada Completa" placeholderTextColor={cores.placeholder}
/>
{/* CAMPOS ESPECÍFICOS PARA ALUNO */}
{/* CAMPOS DINÂMICOS */}
{tipo === 'aluno' && (
<>
<Text style={styles.sectionTitle}>Dados Escolares</Text>
<SectionHeader title="Percurso Escolar" cores={cores} />
<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}
value={ano} onChangeText={setAno} keyboardType="numeric" placeholder="Ano" placeholderTextColor={cores.placeholder}
/>
<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}
value={nEscola} onChangeText={setNEscola} keyboardType="numeric" placeholder="Nº Aluno" placeholderTextColor={cores.placeholder}
/>
</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}
value={curso} onChangeText={setCurso} placeholder="Sigla do Curso (ex: GPSI)" placeholderTextColor={cores.placeholder}
/>
</>
)}
{tipo === 'professor' && (
<>
<SectionHeader title="Docência" cores={cores} />
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={curso} onChangeText={setCurso} placeholder="Departamento / Área Especialidade" placeholderTextColor={cores.placeholder}
/>
</>
)}
{tipo === 'empresa' && (
<>
<SectionHeader title="Dados da Entidade & Tutor" cores={cores} />
<View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={nEscola} onChangeText={setNEscola} keyboardType="numeric" placeholder="NIF" placeholderTextColor={cores.placeholder}
/>
<TextInput
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={setor} onChangeText={setSetor} placeholder="Setor Atividade" placeholderTextColor={cores.placeholder}
/>
</View>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={tutorNome} onChangeText={setTutorNome} placeholder="Nome do Tutor Responsável" placeholderTextColor={cores.placeholder}
/>
<TextInput
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
value={tutorTelefone} onChangeText={setTutorTelefone} keyboardType="phone-pad" placeholder="Contacto do Tutor" placeholderTextColor={cores.placeholder}
/>
</>
)}
@@ -195,10 +285,10 @@ const CriarAluno = () => {
<TouchableOpacity
style={[styles.submitBtn, { backgroundColor: cores.azul }]}
onPress={handleCriar}
onPress={handleCriar}
disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.submitBtnText}>REGISTAR {tipo.toUpperCase()}</Text>}
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.submitBtnText}>REGISTAR NO SISTEMA</Text>}
</TouchableOpacity>
</ScrollView>
@@ -208,20 +298,22 @@ const CriarAluno = () => {
);
};
const SectionHeader = ({ title, cores }: any) => (
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{title}</Text>
);
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;

View File

@@ -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<any>(null);
const [loading, setLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [saving, setSaving] = useState(false);
const [editForm, setEditForm] = useState<AlunoEditForm>({
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<any>(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 (
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
@@ -97,20 +171,19 @@ const DetalhesAlunos = memo(() => {
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
{/* HEADER CLEAN */}
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}>
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
<TouchableOpacity onPress={fetchAluno} style={[styles.btnAction, { borderColor: cores.borda }]}>
<Ionicons name="refresh-outline" size={20} color={cores.azul} />
<TouchableOpacity onPress={() => setModalVisible(true)} style={[styles.btnAction, { borderColor: cores.borda }]}>
<Ionicons name="create-outline" size={22} color={cores.laranja} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 30 }]} showsVerticalScrollIndicator={false}>
{/* PERFIL MINIMALISTA */}
<View style={styles.profileSection}>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarTxt, { color: cores.azul }]}>{aluno?.nome?.charAt(0)}</Text>
@@ -121,17 +194,27 @@ const DetalhesAlunos = memo(() => {
</View>
</View>
{/* DADOS PESSOAIS CARD */}
{/* INFORMAÇÕES PESSOAIS */}
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<DetailRow icon="school-outline" label="Nº Escola" value={aluno?.n_escola} cores={cores} />
<DetailRow icon="mail-outline" label="EmailInstitucional" value={aluno?.perfil?.email} cores={cores}
onPress={() => Linking.openURL(`mailto:${aluno?.perfil?.email}`)} />
<DetailRow icon="call-outline" label="Telemóvel" value={aluno?.perfil?.telefone} cores={cores}
onPress={() => Linking.openURL(`tel:${aluno?.perfil?.telefone}`)} />
<DetailRow
icon="calendar-outline"
label="Nascimento / Idade"
value={aluno?.perfil?.data_nascimento ? `${aluno.perfil.data_nascimento} (${calcularIdade(aluno.perfil.data_nascimento)} anos)` : '-'}
cores={cores}
/>
<DetailRow
icon="mail-outline" label="Email" value={aluno?.perfil?.email} cores={cores}
onPress={aluno?.perfil?.email ? () => Linking.openURL(`mailto:${aluno.perfil.email}`) : null}
/>
<DetailRow
icon="call-outline" label="Telemóvel" value={aluno?.perfil?.telefone} cores={cores}
onPress={aluno?.perfil?.telefone ? () => Linking.openURL(`tel:${aluno.perfil.telefone}`) : null}
/>
<DetailRow icon="location-outline" label="Residência" value={aluno?.perfil?.residencia} cores={cores} ultimo />
</View>
{/* SECÇÃO ESTÁGIO */}
{/* ESTÁGIO */}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Plano de Estágio</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
@@ -143,70 +226,102 @@ const DetalhesAlunos = memo(() => {
<Ionicons name="business" size={28} color="rgba(255,255,255,0.4)" />
<View style={styles.statusBadge}><Text style={styles.statusText}>ATIVO</Text></View>
</View>
<Text style={styles.empresaNome}>{aluno?.estagio?.empresas?.nome || 'Empresa Indefinida'}</Text>
<Text style={styles.empresaNome}>{aluno?.estagio?.empresas?.nome || 'Empresa'}</Text>
<View style={styles.tutorInfo}>
<Text style={styles.miniLabel}>TUTOR NA EMPRESA</Text>
<Text style={styles.tutorNome}>{aluno?.estagio?.empresas?.tutor_nome || 'Não definido'}</Text>
<Text style={styles.miniLabel}>TUTOR</Text>
<Text style={styles.tutorNome}>{aluno?.estagio?.empresas?.tutor_nome || 'N/A'}</Text>
<Text style={styles.tutorTel}>{aluno?.estagio?.empresas?.tutor_telefone || '-'}</Text>
</View>
<View style={styles.horarioBox}>
<Text style={styles.miniLabel}>HORÁRIOS</Text>
{aluno?.horarios && aluno.horarios.length > 0 ? (
aluno.horarios.map((h: string, i: number) => (
<Text key={i} style={styles.horarioTxt}>{h}</Text>
))
) : (
<Text style={styles.horarioTxt}>Não registado</Text>
)}
{aluno?.horarios?.length > 0 ? (
aluno.horarios.map((h: string, i: number) => <Text key={i} style={styles.horarioTxt}>{h}</Text>)
) : <Text style={styles.horarioTxt}>Não definido</Text>}
</View>
<View style={styles.estagioDivider} />
{/* PERÍODO CORRIGIDO: DUAS COLUNAS COM ÍCONE */}
<View style={styles.estagioFooter}>
<View style={styles.periodoCol}>
<Ionicons name="calendar-outline" size={16} color="rgba(255,255,255,0.6)" style={{marginRight: 8}} />
<View>
<Text style={styles.miniLabel}>INÍCIO</Text>
<Text style={styles.footerVal}>{aluno?.estagio?.data_inicio}</Text>
</View>
<Text style={styles.miniLabel}>INÍCIO</Text>
<Text style={styles.footerVal}>{aluno?.estagio?.data_inicio || '-'}</Text>
</View>
<View style={[styles.periodoCol, {alignItems: 'flex-end'}]}>
<View style={{marginRight: 8, alignItems: 'flex-end'}}>
<Text style={styles.miniLabel}>FIM PREVISTO</Text>
<Text style={styles.footerVal}>{aluno?.estagio?.data_fim}</Text>
</View>
<Ionicons name="calendar" size={16} color="rgba(255,255,255,0.6)" />
<Text style={styles.miniLabel}>FIM</Text>
<Text style={styles.footerVal}>{aluno?.estagio?.data_fim || '-'}</Text>
</View>
</View>
</View>
) : (
<View style={[styles.noEstagio, { borderColor: cores.borda, backgroundColor: cores.card }]}>
<Ionicons name="alert-circle-outline" size={24} color={cores.laranja} />
<Text style={[styles.noEstagioTxt, { color: cores.secundario }]}>Sem estágio atribuído</Text>
</View>
)}
</ScrollView>
{/* MODAL DE EDIÇÃO */}
<Modal visible={modalVisible} animationType="slide" transparent>
<View style={styles.modalContainer}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Editar Aluno</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Ionicons name="close" size={28} color={cores.texto} />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
<EditInput label="Nome Completo" value={editForm.nome} onChange={(t: string) => setEditForm({...editForm, nome: t})} cores={cores} />
<EditInput label="Nº Escola" value={editForm.n_escola} onChange={(t: string) => setEditForm({...editForm, n_escola: t})} cores={cores} keyboard="numeric" />
<EditInput label="Turma/Curso" value={editForm.turma_curso} onChange={(t: string) => setEditForm({...editForm, turma_curso: t})} cores={cores} />
<EditInput label="Email" value={editForm.email} onChange={(t: string) => setEditForm({...editForm, email: t})} cores={cores} keyboard="email-address" />
<EditInput label="Telemóvel" value={editForm.telefone} onChange={(t: string) => setEditForm({...editForm, telefone: t})} cores={cores} keyboard="phone-pad" />
<EditInput label="Nascimento (AAAA-MM-DD)" value={editForm.data_nascimento} onChange={(t: string) => setEditForm({...editForm, data_nascimento: t})} cores={cores} keyboard="numeric" />
<EditInput label="Residência" value={editForm.residencia} onChange={(t: string) => setEditForm({...editForm, residencia: t})} cores={cores} />
<TouchableOpacity onPress={handleUpdate} disabled={saving} style={[styles.btnSave, { backgroundColor: cores.azul }]}>
{saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.btnSaveTxt}>Guardar Alterações</Text>}
</TouchableOpacity>
</ScrollView>
</View>
</View>
</Modal>
</SafeAreaView>
</View>
);
});
// --- 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) => (
<View style={{ marginBottom: 15 }}>
<Text style={{ color: cores.secundario, fontSize: 10, fontWeight: '800', marginBottom: 5, textTransform: 'uppercase' }}>{label}</Text>
<TextInput
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto }]}
value={value}
onChangeText={onChange}
keyboardType={keyboard}
placeholderTextColor={cores.secundario}
/>
</View>
);
const DetailRow = ({ icon, label, value, cores, ultimo, onPress }: any) => (
<TouchableOpacity
activeOpacity={onPress ? 0.6 : 1}
onPress={onPress}
style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda + '50' }]}
>
<Ionicons name={icon} size={20} color={cores.azul} style={{ marginRight: 15 }} />
<TouchableOpacity disabled={!onPress} onPress={onPress} style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda + '50' }]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 12 }} />
<View style={{ flex: 1 }}>
<Text style={[styles.rowLabel, { color: cores.secundario }]}>{label}</Text>
<Text style={[styles.rowValue, { color: cores.texto }]}>{value || '-'}</Text>
</View>
{onPress && <Ionicons name="chevron-forward" size={14} color={cores.borda} />}
{onPress && <Ionicons name="chevron-forward" size={14} color={cores.secundario} />}
</TouchableOpacity>
);
@@ -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;

View File

@@ -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<Aluno | null>(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(() => {
</TouchableOpacity>
</View>
{/* SEARCH */}
{/* PESQUISA */}
<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..."
placeholder="Procurar aluno..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
@@ -199,7 +217,7 @@ const ListaAlunosProfessor = memo(() => {
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.idText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
<Ionicons name="ellipsis-vertical" size={18} color={cores.borda} />
<Ionicons name="trash-outline" size={18} color={cores.vermelho} style={{ opacity: 0.5 }} />
</TouchableOpacity>
))}
</View>
@@ -207,18 +225,18 @@ const ListaAlunosProfessor = memo(() => {
/>
)}
{/* MODAL DE ELIMINAÇÃO CUSTOMIZADO */}
{/* MODAL DE ELIMINAÇÃO */}
<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} />
<Ionicons name="trash-bin-outline" size={32} color={cores.vermelho} />
</View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Aluno?</Text>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Permanentemente?</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!
Estás prestes a apagar o perfil de <Text style={{fontWeight:'900', color: cores.texto}}>{alunoParaEliminar?.nome}</Text>.
Isto removerá o acesso à app, dados escolares e estágios. **Vai dar merda** se apagares por engano!
</Text>
<View style={styles.modalButtons}>
@@ -237,7 +255,7 @@ const ListaAlunosProfessor = memo(() => {
{isDeleting ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={[styles.btnText, { color: '#fff' }]}>Eliminar</Text>
<Text style={[styles.btnText, { color: '#fff' }]}>Eliminar Tudo</Text>
)}
</TouchableOpacity>
</View>
@@ -245,9 +263,10 @@ const ListaAlunosProfessor = memo(() => {
</View>
</Modal>
{/* BOTÃO FLUTUANTE */}
<TouchableOpacity
style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]}
onPress={() => router.push('/Professor/Alunos/CriarAluno')} // Altera esta linha
onPress={() => router.push('/Professor/Alunos/CriarAluno')}
>
<Ionicons name="person-add" size={24} color="#fff" />
<Text style={styles.fabText}>Novo Aluno</Text>
@@ -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' },

View File

@@ -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() {
<TouchableOpacity style={[styles.backBtn, { borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>Perfil</Text>
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
<TouchableOpacity
style={[styles.editBtn, { backgroundColor: editando ? cores.laranja : cores.card, borderColor: editando ? cores.laranja : cores.borda }]}
onPress={() => editando ? guardarPerfil() : setEditando(true)}
@@ -164,34 +178,29 @@ export default function PerfilProfessor() {
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
</View>
{editando && (
<View style={[styles.editBadge, { backgroundColor: cores.laranja }]}>
<Ionicons name="camera" size={12} color="#fff" />
</View>
)}
</View>
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
<View style={[styles.roleBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.userRole, { color: cores.azul }]}>{perfil?.curso || 'Professor'}</Text>
<Text style={[styles.userRole, { color: cores.azul }]}>PROFESSOR {perfil?.curso}</Text>
</View>
</View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<ModernInput label="Nome Completo" icon="person" value={perfil?.nome || ''} editable={editando}
<ModernInput label="Nome do Docente" icon="person" value={perfil?.nome || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} />
<ModernInput label="Área de Formação" icon="school" value={perfil?.curso || ''} editable={editando}
<ModernInput label="Curso Associado" icon="school" value={perfil?.curso || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} />
<ModernInput label="Email Institucional" icon="mail" value={perfil?.email || ''} editable={false} cores={cores} />
<ModernInput label="Email de Login" icon="mail" value={perfil?.email || ''} editable={false} cores={cores} />
<View style={styles.row}>
<View style={{ flex: 1, marginRight: 10 }}>
<ModernInput label="Nº Escola" icon="id-card" value={perfil?.n_escola || ''} editable={editando}
<ModernInput label="Nº Mecanográfico" icon="id-card" value={perfil?.n_escola || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} />
</View>
<View style={{ flex: 1.5 }}>
<ModernInput label="Telemóvel" icon="call" value={perfil?.telefone || ''} editable={editando}
<ModernInput label="Contacto" icon="call" value={perfil?.telefone || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} />
</View>
</View>
@@ -203,7 +212,7 @@ export default function PerfilProfessor() {
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
</View>
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar palavra-passe</Text>
<Text style={[styles.menuText, { color: cores.texto }]}>Segurança da Conta</Text>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</TouchableOpacity>
@@ -212,13 +221,13 @@ export default function PerfilProfessor() {
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="power" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar Sessão</Text>
<Text style={[styles.menuText, { color: cores.vermelho }]}>Sair do Sistema</Text>
</TouchableOpacity>
</View>
{editando && (
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setEditando(false); carregarPerfil(); }}>
<Text style={[styles.cancelText, { color: cores.laranja }]}>Cancelar Alterações</Text>
<Text style={[styles.cancelText, { color: cores.laranja }]}>Reverter Alterações</Text>
</TouchableOpacity>
)}
@@ -231,7 +240,7 @@ export default function PerfilProfessor() {
const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
<View style={styles.inputWrapper}>
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
<View style={[styles.inputContainer, { backgroundColor: editable ? '#fff' : cores.azulSuave, borderColor: editable ? cores.laranja : 'transparent' }]}>
<View style={[styles.inputContainer, { backgroundColor: editable ? cores.fundo : cores.azulSuave, borderColor: editable ? cores.laranja : 'transparent' }]}>
<Ionicons name={icon} size={18} color={editable ? cores.laranja : cores.azul} style={{ marginRight: 12 }} />
<TextInput {...props} editable={editable} style={[styles.textInput, { color: cores.texto }]} />
</View>
@@ -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 },