criar/dados aluno
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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 }]}>Nº 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 },
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 }]}>Nº {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' },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user