This commit is contained in:
2026-03-10 17:15:26 +00:00
parent 6cdbe14875
commit c501fa7801
7 changed files with 625 additions and 574 deletions

View File

@@ -20,7 +20,6 @@ LocaleConfig.locales['pt'] = {
};
LocaleConfig.defaultLocale = 'pt';
// --- FUNÇÃO PARA CALCULAR FERIADOS (Nacionais + Vila do Conde) ---
const getFeriadosMap = (ano: number) => {
const f: Record<string, string> = {
[`${ano}-01-01`]: "Ano Novo",
@@ -107,12 +106,16 @@ const AlunoHome = memo(() => {
const diaSemana = data.getDay();
const ehFimDeSemana = diaSemana === 0 || diaSemana === 6;
const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
// CORREÇÃO: Verifica se o dia selecionado é exatamente HOJE
const ehHoje = selectedDate === hojeStr;
const ehFuturo = selectedDate > hojeStr;
const nomeFeriado = feriadosMap[selectedDate];
return {
valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado,
podeMarcarPresenca: !ehFimDeSemana && !foraDoIntervalo && !ehFuturo && !nomeFeriado,
// Só permite presença se for o dia atual
podeMarcarPresenca: ehHoje && !foraDoIntervalo && !nomeFeriado,
ehFuturo,
nomeFeriado
};
@@ -133,7 +136,7 @@ const AlunoHome = memo(() => {
}, [presencas, faltas, sumarios, faltasJustificadas, selectedDate, listaFeriados]);
const handlePresenca = async () => {
if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "Data inválida.");
if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "A presença só pode ser marcada no próprio dia.");
const novas = { ...presencas, [selectedDate]: true };
setPresencas(novas);
await AsyncStorage.setItem('@presencas', JSON.stringify(novas));
@@ -202,7 +205,7 @@ const AlunoHome = memo(() => {
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card }]}>
<Calendar
key={isDarkMode ? 'dark' : 'light'} // 🔹 Força re-render no tema
key={isDarkMode ? 'dark' : 'light'}
theme={{ calendarBackground: themeStyles.card, dayTextColor: themeStyles.texto, monthTextColor: themeStyles.texto, todayTextColor: '#0d6efd', arrowColor: '#0d6efd' }}
markedDates={diasMarcados}
onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }}

View File

@@ -1,234 +1,101 @@
//PÁGINA PERFIL DO ALUNO
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import {
Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet,
Text, TouchableOpacity, View
} from 'react-native';
import { ActivityIndicator, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
export default function Perfil() {
export default function PerfilAluno() {
const { isDarkMode } = useTheme();
const router = useRouter();
const [loading, setLoading] = useState(true);
const [perfil, setPerfil] = useState<any>(null);
const [estagio, setEstagio] = useState<any>(null);
// Estados para dados dinâmicos e estatísticas
const [datas, setDatas] = useState({ inicio: '05/01/2026', fim: '30/05/2026' });
const [stats, setStats] = useState({
horasConcluidas: 0,
faltasTotais: 0,
faltasJustificadas: 0,
horasFaltam: 300
});
useEffect(() => {
carregarDados();
}, []);
async function carregarDados() {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
// 1. Dados do Perfil
const { data: prof } = await supabase.from('profiles').select('*').eq('id', user.id).single();
setPerfil(prof);
// 2. Dados do Estágio e Empresa (Relacionados)
const { data: est } = await supabase
.from('estagios')
.select('*, empresas(*)')
.eq('aluno_id', user.id)
.single();
setEstagio(est);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}
const themeStyles = {
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#000',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
borda: isDarkMode ? '#333' : '#f1f3f5',
};
useEffect(() => {
const carregarECalcular = async () => {
try {
const [config, presencasRaw, faltasRaw, justRaw] = await Promise.all([
AsyncStorage.getItem('@dados_estagio'),
AsyncStorage.getItem('@presencas'),
AsyncStorage.getItem('@faltas'),
AsyncStorage.getItem('@justificacoes')
]);
// 1. Carregar Configurações de Datas
if (config) {
const p = JSON.parse(config);
setDatas({
inicio: p.inicio || '05/01/2026',
fim: p.fim || '30/05/2026'
});
}
// 2. Calcular estatísticas baseadas nos objetos do calendário
const presencas = presencasRaw ? JSON.parse(presencasRaw) : {};
const faltas = faltasRaw ? JSON.parse(faltasRaw) : {};
const justificacoes = justRaw ? JSON.parse(justRaw) : {};
const totalDiasPresenca = Object.keys(presencas).length;
const totalFaltas = Object.keys(faltas).length;
const totalJustificadas = Object.keys(justificacoes).length;
const horasFeitas = totalDiasPresenca * 7; // 7h por dia
const totalHorasEstagio = 300;
setStats({
horasConcluidas: horasFeitas,
faltasTotais: totalFaltas,
faltasJustificadas: totalJustificadas,
horasFaltam: Math.max(0, totalHorasEstagio - horasFeitas)
});
} catch (e) {
console.error("Erro ao sincronizar dados do perfil", e);
}
};
carregarECalcular();
}, []);
if (loading) return <ActivityIndicator style={{flex:1}} color="#0d6efd" />;
return (
<SafeAreaView style={[styles.safe, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: themeStyles.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: themeStyles.texto }]}>Perfil do Aluno</Text>
<View style={styles.spacer} />
</View>
<ScrollView contentContainerStyle={styles.container}>
<Text style={[styles.tituloGeral, { color: themeStyles.texto }]}>O Meu Perfil</Text>
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
{/* Dados Pessoais - RESTAURADO TOTALMENTE */}
{/* Dados Pessoais vindos da tabela PROFILES */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Dados Pessoais</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Nome</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Ricardo Gomes</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Idade</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>17 anos</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Residência</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Junqueira, Vila do Conde</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Telemóvel</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>915783648</Text>
<LabelValor label="Nome" valor={perfil?.nome} theme={themeStyles} />
<LabelValor label="Idade" valor={`${perfil?.idade} anos`} theme={themeStyles} />
<LabelValor label="Residência" valor={perfil?.residencia} theme={themeStyles} />
<LabelValor label="Telemóvel" valor={perfil?.telefone} theme={themeStyles} />
</View>
{/* NOVA SECÇÃO: Assiduidade Dinâmica */}
{/* Dados da Empresa vindos da tabela EMPRESAS via Estágio */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Assiduidade</Text>
<View style={styles.rowStats}>
<View style={styles.itemStat}>
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Faltas Totais</Text>
<Text style={[styles.valor, { color: '#dc3545', fontSize: 18 }]}>{stats.faltasTotais}</Text>
</View>
<View style={styles.itemStat}>
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Justificadas</Text>
<Text style={[styles.valor, { color: '#198754', fontSize: 18 }]}>{stats.faltasJustificadas}</Text>
</View>
<View style={styles.itemStat}>
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Ñ Justif.</Text>
<Text style={[styles.valor, { color: themeStyles.texto, fontSize: 18 }]}>
{stats.faltasTotais - stats.faltasJustificadas}
</Text>
</View>
</View>
</View>
{/* Empresa de Estágio - RESTAURADO TOTALMENTE */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Empresa de Estágio</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Curso</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Técnico de Informática</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Empresa</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Tech Solutions, Lda</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Morada</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Rua das papoilas, nº67</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Tutor</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>Nicolau de Sousa</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Telemóvel</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>917892748</Text>
</View>
{/* Dados do Estágio - HORAS DINÂMICAS */}
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
<Text style={styles.tituloCard}>Dados do Estágio</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Início do Estágio</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>{datas.inicio}</Text>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Fim do Estágio</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>{datas.fim}</Text>
<View style={[styles.estatisticasHoras, { borderBottomColor: themeStyles.borda }]}>
<View>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Totais</Text>
<Text style={[styles.valor, { color: themeStyles.texto }]}>300h</Text>
</View>
<View>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Concluídas</Text>
<Text style={[styles.valor, {color: '#198754'}]}>{stats.horasConcluidas}h</Text>
</View>
<View>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Faltam</Text>
<Text style={[styles.valor, {color: '#dc3545'}]}>{stats.horasFaltam}h</Text>
</View>
</View>
<Text style={[styles.labelHorario, { color: themeStyles.texto }]}>Horário Semanal</Text>
<View style={[styles.tabela, { borderColor: themeStyles.borda }]}>
<View style={[styles.linhaTab, { backgroundColor: isDarkMode ? '#2c2c2c' : '#f8f9fa', borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.celulaHeader, { color: themeStyles.texto }]}>Período</Text>
<Text style={[styles.celulaHeader, { color: themeStyles.texto }]}>Horário</Text>
</View>
<View style={[styles.linhaTab, { borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Manhã</Text>
<Text style={[styles.celulaValor, { color: themeStyles.texto }]}>09:30 - 13:00</Text>
</View>
<View style={[styles.linhaTab, { backgroundColor: isDarkMode ? '#252525' : '#fdfcfe', borderBottomColor: themeStyles.borda }]}>
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Almoço</Text>
<Text style={[styles.celulaValor, { fontWeight: '400', color: themeStyles.textoSecundario }]}>13:00 - 14:30</Text>
</View>
<View style={[styles.linhaTab, { borderBottomWidth: 0 }]}>
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Tarde</Text>
<Text style={[styles.celulaValor, { color: themeStyles.texto }]}>14:30 - 17:30</Text>
</View>
</View>
<Text style={styles.notaTotal}>Total: 7 horas diárias por presença</Text>
<Text style={styles.tituloCard}>Local de Estágio</Text>
<LabelValor label="Empresa" valor={estagio?.empresas?.nome} theme={themeStyles} />
<LabelValor label="Tutor" valor={estagio?.empresas?.tutor_nome} theme={themeStyles} />
<LabelValor label="Contacto Tutor" valor={estagio?.empresas?.tutor_telefone} theme={themeStyles} />
<LabelValor label="Morada" valor={estagio?.empresas?.morada} theme={themeStyles} />
</View>
<TouchableOpacity
style={styles.btnSair}
onPress={async () => { await supabase.auth.signOut(); router.replace('/'); }}
>
<Text style={{color: '#fff', fontWeight: 'bold'}}>Sair da Conta</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
}
function LabelValor({ label, valor, theme }: any) {
return (
<View style={{ marginBottom: 12 }}>
<Text style={{ fontSize: 11, color: theme.textoSecundario, textTransform: 'uppercase' }}>{label}</Text>
<Text style={{ fontSize: 16, color: theme.texto, fontWeight: '600' }}>{valor || '---'}</Text>
</View>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 },
spacer: { width: 40 },
container: { padding: 20, gap: 20, paddingBottom: 40 },
tituloGeral: { fontSize: 22, fontWeight: 'bold' },
card: { padding: 20, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
tituloCard: { fontSize: 18, fontWeight: 'bold', color: '#0d6efd', textAlign: 'center', marginBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f3f5', paddingBottom: 8 },
label: { marginTop: 12, fontSize: 13 },
valor: { fontSize: 16, fontWeight: '600' },
labelHorario: { fontSize: 16, fontWeight: 'bold', marginTop: 20, marginBottom: 10, textAlign: 'center' },
tabela: { borderWidth: 1, borderRadius: 8, overflow: 'hidden' },
linhaTab: { flexDirection: 'row', borderBottomWidth: 1, paddingVertical: 8, alignItems: 'center' },
celulaHeader: { flex: 1, fontWeight: 'bold', textAlign: 'center', fontSize: 13 },
celulaLabel: { flex: 1, paddingLeft: 12, fontSize: 14 },
celulaValor: { flex: 1, textAlign: 'center', fontSize: 14, fontWeight: '600' },
notaTotal: { textAlign: 'center', fontSize: 12, color: '#0d6efd', marginTop: 8, fontWeight: '500' },
estatisticasHoras: { flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, paddingBottom: 15, marginTop: 5 },
rowStats: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 },
itemStat: { alignItems: 'center', flex: 1 }
safe: { flex: 1 },
container: { padding: 20, gap: 20 },
tituloGeral: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
card: { padding: 20, borderRadius: 16, elevation: 2 },
tituloCard: { fontSize: 14, fontWeight: 'bold', color: '#0d6efd', marginBottom: 15, textTransform: 'uppercase' },
btnSair: { backgroundColor: '#dc3545', padding: 18, borderRadius: 15, alignItems: 'center', marginTop: 10 }
});

View File

@@ -1,3 +1,4 @@
// app/Professor/Alunos/DetalhesAluno.tsx
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { memo, useEffect, useState } from 'react';
@@ -13,6 +14,18 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
// Definindo a interface para o estado do aluno
interface AlunoEstado {
id: string;
nome: string;
n_escola: string;
turma_curso: string;
email: string;
telefone: string;
residencia: string;
idade: string;
}
const DetalhesAlunos = memo(() => {
const router = useRouter();
const params = useLocalSearchParams();
@@ -32,7 +45,7 @@ const DetalhesAlunos = memo(() => {
? params.alunoId[0]
: null;
const [aluno, setAluno] = useState<any>(null);
const [aluno, setAluno] = useState<AlunoEstado | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -47,42 +60,45 @@ const DetalhesAlunos = memo(() => {
try {
setLoading(true);
// 1⃣ Buscar dados do aluno
const { data: alunoData, error: alunoError } = await supabase
const { data, error } = await supabase
.from('alunos')
.select('id, nome, n_escola, turma_curso')
.select(`
id,
nome,
n_escola,
turma_curso,
profiles!alunos_profile_id_fkey (
email,
telefone,
residencia,
idade
)
`)
.eq('id', alunoId)
.single();
if (alunoError || !alunoData) {
console.log('Erro ao buscar aluno:', alunoError);
setAluno(null);
if (error) {
console.log('Erro ao buscar:', error.message);
setLoading(false);
return;
}
// 2⃣ Buscar dados do profile como array
const { data: perfilDataArray, error: perfilError } = await supabase
.from('profiles')
.select('email, telefone, residencia, idade')
.eq('n_escola', alunoData.n_escola);
if (data) {
// CORREÇÃO AQUI: Forçamos o TypeScript a tratar profiles como um objeto
// para que ele permita acessar email, telefone, etc.
const perfil = data.profiles as any;
if (perfilError) console.log('Erro ao buscar profile:', perfilError);
// Pega o primeiro registro ou undefined
const perfilData = perfilDataArray?.[0];
// 3⃣ Combinar dados
setAluno({
id: alunoData.id,
nome: alunoData.nome,
n_escola: alunoData.n_escola,
turma_curso: alunoData.turma_curso,
email: perfilData?.email ?? '-',
telefone: perfilData?.telefone ?? '-',
residencia: perfilData?.residencia ?? '-',
idade: perfilData?.idade?.toString() ?? '-',
});
setAluno({
id: String(data.id),
nome: data.nome || 'Sem nome',
n_escola: String(data.n_escola || '-'),
turma_curso: data.turma_curso || '-',
email: perfil?.email ?? '-',
telefone: perfil?.telefone ?? '-',
residencia: perfil?.residencia ?? '-',
idade: perfil?.idade ? String(perfil.idade) : '-',
});
}
setLoading(false);
} catch (err) {
@@ -136,10 +152,10 @@ const DetalhesAlunos = memo(() => {
});
const renderCampo = (label: string, valor: string, colors: any) => (
<>
<View key={label} style={{ marginBottom: 15 }}>
<Text style={[styles.label, { color: colors.label }]}>{label}</Text>
<Text style={[styles.valor, { color: colors.text }]}>{valor}</Text>
</>
</View>
);
export default DetalhesAlunos;
@@ -147,10 +163,24 @@ export default DetalhesAlunos;
const styles = StyleSheet.create({
safe: { flex: 1 },
center: { marginTop: 50, textAlign: 'center', fontSize: 16 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12 },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12
},
titulo: { fontSize: 20, fontWeight: 'bold' },
container: { padding: 16 },
card: { padding: 16, borderRadius: 12, elevation: 2 },
label: { fontSize: 12, marginTop: 10 },
card: {
padding: 16,
borderRadius: 12,
elevation: 2,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 }
},
label: { fontSize: 12, marginBottom: 2, textTransform: 'uppercase', letterSpacing: 0.5 },
valor: { fontSize: 16, fontWeight: '600' },
});

View File

@@ -1,51 +1,39 @@
// app/Professor/Estagios.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
Alert,
Modal,
ActivityIndicator,
Alert, Modal,
Platform,
SafeAreaView,
ScrollView,
SafeAreaView, ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
StyleSheet, Text, TextInput, TouchableOpacity, View
} from 'react-native';
import { useTheme } from '../../../themecontext'; // mesmo theme context das definições
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
interface Aluno {
id: number;
nome: string;
// --- Interfaces ---
interface Horario {
id?: number;
periodo: string;
hora_inicio: string;
hora_fim: string;
}
interface Empresa {
id: number;
nome: string;
}
interface Aluno { id: string; nome: string; turma_curso: string; }
interface Empresa { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; }
interface Estagio {
id: number;
alunoId: number;
empresaId: number;
alunoNome: string;
empresaNome: string;
id: string;
aluno_id: string;
empresa_id: string;
data_inicio: string;
data_fim: string;
horas_diarias?: string;
alunos: { nome: string; turma_curso: string };
empresas: { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string };
}
// Dados simulados
const alunosData: Aluno[] = [
{ id: 1, nome: 'João Silva' },
{ id: 2, nome: 'Maria Fernandes' },
];
const empresasData: Empresa[] = [
{ id: 1, nome: 'Empresa A' },
{ id: 2, nome: 'Empresa B' },
];
export default function Estagios() {
const router = useRouter();
const { isDarkMode } = useTheme();
@@ -58,168 +46,319 @@ export default function Estagios() {
border: isDarkMode ? '#343a40' : '#ced4da',
azul: '#0d6efd',
vermelho: '#dc3545',
inputBg: isDarkMode ? '#2c2c2c' : '#f8f9fa',
}), [isDarkMode]);
// Estados
// --- Estados ---
const [estagios, setEstagios] = useState<Estagio[]>([]);
const [alunos, setAlunos] = useState<Aluno[]>([]);
const [empresas, setEmpresas] = useState<Empresa[]>([]);
const [loading, setLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [passo, setPasso] = useState(1);
const [alunoSelecionado, setAlunoSelecionado] = useState<Aluno | null>(null);
const [empresaSelecionada, setEmpresaSelecionada] = useState<Empresa | null>(null);
const [editandoEstagio, setEditandoEstagio] = useState<Estagio | null>(null);
const [searchText, setSearchText] = useState('');
const [dataInicio, setDataInicio] = useState('');
const [dataFim, setDataFim] = useState('');
const [horarios, setHorarios] = useState<Horario[]>([]);
// Filtrar estágios por busca
const estagiosFiltrados = estagios.filter(e =>
e.alunoNome.toLowerCase().includes(searchText.toLowerCase()) ||
e.empresaNome.toLowerCase().includes(searchText.toLowerCase())
);
const [searchMain, setSearchMain] = useState('');
const [searchAluno, setSearchAluno] = useState('');
const [searchEmpresa, setSearchEmpresa] = useState('');
const abrirModalNovo = () => {
// --- Cálculo de Horas Diárias ---
const totalHorasDiarias = useMemo(() => {
let totalMinutos = 0;
horarios.forEach(h => {
const [hIni, mIni] = h.hora_inicio.split(':').map(Number);
const [hFim, mFim] = h.hora_fim.split(':').map(Number);
if (!isNaN(hIni) && !isNaN(hFim)) {
const inicio = hIni * 60 + (mIni || 0);
const fim = hFim * 60 + (mFim || 0);
if (fim > inicio) totalMinutos += (fim - inicio);
}
});
const h = Math.floor(totalMinutos / 60);
const m = totalMinutos % 60;
return m > 0 ? `${h}h${m}m` : `${h}h`;
}, [horarios]);
// --- Funções de Dados ---
useEffect(() => { fetchDados(); }, []);
const fetchDados = async () => {
try {
setLoading(true);
const [resEstagios, resAlunos, resEmpresas] = await Promise.all([
supabase.from('estagios').select('*, alunos(nome, turma_curso), empresas(*)'),
supabase.from('alunos').select('id, nome, turma_curso').order('nome'),
supabase.from('empresas').select('*').order('nome')
]);
if (resEstagios.data) setEstagios(resEstagios.data);
if (resAlunos.data) setAlunos(resAlunos.data);
if (resEmpresas.data) setEmpresas(resEmpresas.data);
} catch (e) { console.error(e); } finally { setLoading(false); }
};
const carregarHorarios = async (estagioId: string) => {
const { data } = await supabase.from('horarios_estagio').select('*').eq('estagio_id', estagioId);
if (data) setHorarios(data);
};
const handleFecharModal = () => {
setModalVisible(false);
setEditandoEstagio(null);
setAlunoSelecionado(null);
setEmpresaSelecionada(null);
setEditandoEstagio(null);
setModalVisible(true);
setHorarios([]);
setDataInicio('');
setDataFim('');
setPasso(1);
};
const salvarEstagio = () => {
if (!alunoSelecionado || !empresaSelecionada) {
Alert.alert('Erro', 'Selecione um aluno e uma empresa existentes.');
return;
const eliminarEstagio = (id: string) => {
Alert.alert("Eliminar Estágio", "Deseja remover este estágio e todos os seus horários?", [
{ text: "Cancelar", style: "cancel" },
{ text: "Eliminar", style: "destructive", onPress: async () => {
await supabase.from('estagios').delete().eq('id', id);
fetchDados();
}}
]);
};
const salvarEstagio = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (empresaSelecionada) {
await supabase.from('empresas').update({
morada: empresaSelecionada.morada,
tutor_nome: empresaSelecionada.tutor_nome,
tutor_telefone: empresaSelecionada.tutor_telefone
}).eq('id', empresaSelecionada.id);
}
if (editandoEstagio) {
// Editar estágio existente
setEstagios(estagios.map(e => e.id === editandoEstagio.id ? {
...e,
alunoId: alunoSelecionado.id,
empresaId: empresaSelecionada.id,
alunoNome: alunoSelecionado.nome,
empresaNome: empresaSelecionada.nome
} : e));
} else {
// Criar novo estágio
const novo: Estagio = {
id: Date.now(),
alunoId: alunoSelecionado.id,
empresaId: empresaSelecionada.id,
alunoNome: alunoSelecionado.nome,
empresaNome: empresaSelecionada.nome
};
setEstagios([...estagios, novo]);
const payloadEstagio = {
aluno_id: alunoSelecionado?.id,
empresa_id: empresaSelecionada?.id,
professor_id: user?.id,
data_inicio: dataInicio || new Date().toISOString().split('T')[0],
data_fim: dataFim || null,
horas_diarias: totalHorasDiarias,
estado: 'Ativo',
};
const { data: estData, error: errE } = editandoEstagio
? await supabase.from('estagios').update(payloadEstagio).eq('id', editandoEstagio.id).select().single()
: await supabase.from('estagios').insert([payloadEstagio]).select().single();
if (errE) return Alert.alert("Erro", errE.message);
const currentId = editandoEstagio?.id || estData.id;
await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId);
if (horarios.length > 0) {
const payloadH = horarios.map(h => ({
estagio_id: currentId,
periodo: h.periodo,
hora_inicio: h.hora_inicio,
hora_fim: h.hora_fim
}));
await supabase.from('horarios_estagio').insert(payloadH);
}
setModalVisible(false);
handleFecharModal();
fetchDados();
};
const editarEstagio = (e: Estagio) => {
setEditandoEstagio(e);
setAlunoSelecionado(alunosData.find(a => a.id === e.alunoId) || null);
setEmpresaSelecionada(empresasData.find(emp => emp.id === e.empresaId) || null);
setModalVisible(true);
};
// --- Filtros ---
const alunosAgrupados = useMemo(() => {
const groups: Record<string, Aluno[]> = {};
alunos.filter(a => a.nome.toLowerCase().includes(searchAluno.toLowerCase())).forEach(a => {
const k = a.turma_curso || 'Sem Turma';
if (!groups[k]) groups[k] = [];
groups[k].push(a);
});
return groups;
}, [alunos, searchAluno]);
const empresasAgrupadas = useMemo(() => {
const groups: Record<string, Empresa[]> = {};
empresas.filter(e => e.nome.toLowerCase().includes(searchEmpresa.toLowerCase())).forEach(e => {
const k = e.curso || 'Geral';
if (!groups[k]) groups[k] = [];
groups[k].push(e);
});
return groups;
}, [empresas, searchEmpresa]);
if (loading) return <View style={{flex:1, justifyContent:'center', backgroundColor:cores.fundo}}><ActivityIndicator size="large" color={cores.azul}/></View>;
return (
<SafeAreaView style={[styles.container, { backgroundColor: cores.fundo, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={cores.fundo} />
<ScrollView contentContainerStyle={styles.content}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="arrow-back-outline" size={26} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Estágios</Text>
<View style={{ width: 26 }} />
</View>
<SafeAreaView style={[styles.container, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}><Ionicons name="arrow-back" size={26} color={cores.texto}/></TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Estágios</Text>
<TouchableOpacity onPress={fetchDados}><Ionicons name="refresh" size={24} color={cores.azul}/></TouchableOpacity>
</View>
{/* Pesquisa */}
<TextInput
style={[styles.searchInput, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.border }]}
placeholder="Procurar estágio..."
<ScrollView contentContainerStyle={{ padding: 20 }}>
<TextInput
placeholder="Procurar aluno..."
placeholderTextColor={cores.textoSecundario}
value={searchText}
onChangeText={setSearchText}
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.border }]}
onChangeText={setSearchMain}
/>
{/* Lista de estágios */}
<View style={{ marginTop: 16 }}>
{estagiosFiltrados.map(e => (
<TouchableOpacity key={e.id} style={[styles.card, { backgroundColor: cores.card }]} onPress={() => editarEstagio(e)}>
<Text style={[styles.cardTitle, { color: cores.texto }]}>{e.alunoNome}</Text>
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]}>{e.empresaNome}</Text>
<Ionicons name="create-outline" size={20} color={cores.azul} />
</TouchableOpacity>
))}
{estagiosFiltrados.length === 0 && <Text style={{ color: cores.textoSecundario, marginTop: 12 }}>Nenhum estágio encontrado.</Text>}
</View>
{/* Botão Novo Estágio */}
<TouchableOpacity style={[styles.newButton, { backgroundColor: cores.azul }]} onPress={abrirModalNovo}>
<Ionicons name="add" size={20} color="#fff" />
<Text style={styles.newButtonText}>Novo Estágio</Text>
</TouchableOpacity>
{/* Modal de criação/edição */}
<Modal visible={modalVisible} transparent animationType="slide">
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>{editandoEstagio ? 'Editar Estágio' : 'Novo Estágio'}</Text>
{/* Dropdown Aluno */}
<Text style={[styles.modalLabel, { color: cores.textoSecundario }]}>Aluno</Text>
{alunosData.map(a => (
<TouchableOpacity
key={a.id}
style={[styles.modalOption, { backgroundColor: alunoSelecionado?.id === a.id ? cores.azul : cores.card }]}
onPress={() => setAlunoSelecionado(a)}
>
<Text style={{ color: alunoSelecionado?.id === a.id ? '#fff' : cores.texto }}>{a.nome}</Text>
</TouchableOpacity>
))}
{/* Dropdown Empresa */}
<Text style={[styles.modalLabel, { color: cores.textoSecundario, marginTop: 12 }]}>Empresa</Text>
{empresasData.map(emp => (
<TouchableOpacity
key={emp.id}
style={[styles.modalOption, { backgroundColor: empresaSelecionada?.id === emp.id ? cores.azul : cores.card }]}
onPress={() => setEmpresaSelecionada(emp)}
>
<Text style={{ color: empresaSelecionada?.id === emp.id ? '#fff' : cores.texto }}>{emp.nome}</Text>
</TouchableOpacity>
))}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }}>
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.modalButton, { backgroundColor: cores.vermelho }]}>
<Text style={{ color: '#fff', fontWeight: '600' }}>Cancelar</Text>
</TouchableOpacity>
<TouchableOpacity onPress={salvarEstagio} style={[styles.modalButton, { backgroundColor: cores.azul }]}>
<Text style={{ color: '#fff', fontWeight: '600' }}>Salvar</Text>
</TouchableOpacity>
{estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).map(e => (
<View key={e.id} style={[styles.card, { backgroundColor: cores.card }]}>
<TouchableOpacity style={{ flex: 1 }} onPress={() => {
setEditandoEstagio(e);
setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null);
setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null);
setDataInicio(e.data_inicio || '');
setDataFim(e.data_fim || '');
carregarHorarios(e.id);
setPasso(2);
setModalVisible(true);
}}>
<Text style={[styles.cardTitle, { color: cores.texto }]}>{e.alunos?.nome}</Text>
<View style={{flexDirection: 'row', gap: 10, marginTop: 4}}>
<Text style={{ color: cores.azul, fontSize: 11, fontWeight: '700' }}>{e.alunos?.turma_curso}</Text>
<Text style={{ color: cores.textoSecundario, fontSize: 11 }}> {e.horas_diarias || '0h'}/dia</Text>
</View>
</View>
<Text style={{ color: cores.textoSecundario, marginTop: 4, fontSize: 13 }}>🏢 {e.empresas?.nome}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => eliminarEstagio(e.id)}><Ionicons name="trash-outline" size={20} color={cores.vermelho} /></TouchableOpacity>
</View>
</Modal>
))}
<TouchableOpacity style={[styles.btnNovo, { backgroundColor: cores.azul }]} onPress={() => {
setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true);
}}>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>+ Novo Estágio</Text>
</TouchableOpacity>
</ScrollView>
<Modal visible={modalVisible} animationType="slide" transparent>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<ScrollView showsVerticalScrollIndicator={false}>
{passo === 1 ? (
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Passo 1: Seleção</Text>
<Text style={styles.label}>Aluno</Text>
<View style={styles.selector}><ScrollView nestedScrollEnabled style={{maxHeight: 180}}>
{Object.keys(alunosAgrupados).map(t => (
<View key={t}><Text style={styles.groupHead}>{t}</Text>
{alunosAgrupados[t].map(a => (
<TouchableOpacity key={a.id} style={[styles.item, alunoSelecionado?.id === a.id && {backgroundColor: cores.azul}]} onPress={() => setAlunoSelecionado(a)}>
<Text style={{color: alunoSelecionado?.id === a.id ? '#fff' : cores.texto}}>{a.nome}</Text>
</TouchableOpacity>
))}
</View>
))}
</ScrollView></View>
<Text style={[styles.label, {marginTop: 15}]}>Empresa</Text>
<View style={styles.selector}><ScrollView nestedScrollEnabled style={{maxHeight: 180}}>
{Object.keys(empresasAgrupadas).map(c => (
<View key={c}><Text style={styles.groupHead}>{c}</Text>
{empresasAgrupadas[c].map(emp => (
<TouchableOpacity key={emp.id} style={[styles.item, empresaSelecionada?.id === emp.id && {backgroundColor: cores.azul}]} onPress={() => setEmpresaSelecionada(emp)}>
<Text style={{color: empresaSelecionada?.id === emp.id ? '#fff' : cores.texto}}>{emp.nome}</Text>
</TouchableOpacity>
))}
</View>
))}
</ScrollView></View>
<View style={styles.modalFooter}>
<TouchableOpacity onPress={handleFecharModal} style={[styles.btnModal, {backgroundColor: cores.vermelho}]}><Text style={{color:'#fff'}}>Sair</Text></TouchableOpacity>
<TouchableOpacity onPress={() => { if(alunoSelecionado && empresaSelecionada) setPasso(2); }} style={[styles.btnModal, {backgroundColor: cores.azul}]}><Text style={{color:'#fff'}}>Configurar</Text></TouchableOpacity>
</View>
</View>
) : (
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Passo 2: Detalhes</Text>
<View style={[styles.confirmBox, { borderColor: cores.border }]}>
<Text style={styles.confirmTitle}>DURAÇÃO E HORAS</Text>
<View style={{flexDirection: 'row', gap: 10, marginBottom: 10}}>
<TextInput style={[styles.editInput, {flex:1, color: cores.texto}]} value={dataInicio} onChangeText={setDataInicio} placeholder="Início (AAAA-MM-DD)"/>
<TextInput style={[styles.editInput, {flex:1, color: cores.texto}]} value={dataFim} onChangeText={setDataFim} placeholder="Fim (AAAA-MM-DD)"/>
</View>
<View style={styles.badgeHoras}><Text style={styles.badgeText}>Total Diário: {totalHorasDiarias}</Text></View>
</View>
<View style={[styles.confirmBox, { borderColor: cores.border, marginTop: 15 }]}>
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text style={styles.confirmTitle}>HORÁRIOS</Text>
<TouchableOpacity onPress={() => setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}><Ionicons name="add-circle" size={24} color={cores.azul}/></TouchableOpacity>
</View>
{horarios.map((h, i) => (
<View key={i} style={styles.horarioRow}>
<TextInput style={[styles.miniInput, {color: cores.texto, flex: 1.2}]} value={h.periodo} onChangeText={(t) => { const n = [...horarios]; n[i].periodo = t; setHorarios(n); }} />
<TextInput style={[styles.miniInput, {color: cores.texto}]} value={h.hora_inicio} onChangeText={(t) => { const n = [...horarios]; n[i].hora_inicio = t; setHorarios(n); }} />
<Text style={{color: cores.textoSecundario}}>às</Text>
<TextInput style={[styles.miniInput, {color: cores.texto}]} value={h.hora_fim} onChangeText={(t) => { const n = [...horarios]; n[i].hora_fim = t; setHorarios(n); }} />
<TouchableOpacity onPress={() => setHorarios(horarios.filter((_, idx) => idx !== i))}><Ionicons name="close-circle" size={20} color={cores.vermelho} /></TouchableOpacity>
</View>
))}
</View>
<View style={[styles.confirmBox, { borderColor: cores.border, marginTop: 15 }]}>
<Text style={styles.confirmTitle}>TUTOR</Text>
<TextInput style={[styles.editInput, {color: cores.texto}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome"/>
<TextInput style={[styles.editInput, {color: cores.texto, marginTop: 8}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/>
</View>
<View style={styles.modalFooter}>
<TouchableOpacity
onPress={() => editandoEstagio ? handleFecharModal() : setPasso(1)}
style={[styles.btnModal, {backgroundColor: cores.textoSecundario}]}
>
<Text style={{color:'#fff'}}>{editandoEstagio ? 'Cancelar' : 'Voltar'}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={salvarEstagio} style={[styles.btnModal, {backgroundColor: cores.azul}]}><Text style={{color:'#fff'}}>Gravar</Text></TouchableOpacity>
</View>
</View>
)}
</ScrollView>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
// Estilos
const styles = StyleSheet.create({
container: { flex: 1 },
content: { padding: 24 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
title: { fontSize: 24, fontWeight: '700' },
searchInput: { borderWidth: 1, borderRadius: 12, padding: 12, fontSize: 16, marginTop: 12 },
card: { padding: 16, borderRadius: 14, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, elevation: 2 },
cardTitle: { fontSize: 16, fontWeight: '700' },
cardSubtitle: { fontSize: 14 },
newButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 16, borderRadius: 14, marginTop: 16 },
newButtonText: { color: '#fff', fontWeight: '700', marginLeft: 8 },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', padding: 24 },
modalContent: { borderRadius: 16, padding: 20 },
modalTitle: { fontSize: 18, fontWeight: '700', marginBottom: 12 },
modalLabel: { fontSize: 14, fontWeight: '600', marginBottom: 8 },
modalOption: { padding: 12, borderRadius: 10, marginBottom: 6 },
modalButton: { padding: 12, borderRadius: 12, flex: 1, alignItems: 'center', marginHorizontal: 4 }
});
container: {
flex: 1,
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
},
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 },
title: { fontSize: 22, fontWeight: 'bold' },
input: { borderWidth: 1, borderRadius: 12, padding: 12, marginBottom: 15 },
card: { padding: 16, borderRadius: 15, flexDirection: 'row', alignItems: 'center', marginBottom: 10, elevation: 2 },
cardTitle: { fontSize: 16, fontWeight: 'bold' },
btnNovo: { padding: 18, borderRadius: 12, alignItems: 'center', marginTop: 10 },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 20 },
modalContent: { borderRadius: 20, padding: 20, maxHeight: '90%' },
modalTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 20, textAlign: 'center' },
label: { fontSize: 14, fontWeight: 'bold', marginBottom: 5 },
selector: { borderWidth: 1, borderColor: '#eee', borderRadius: 10, overflow: 'hidden' },
groupHead: { fontSize: 11, backgroundColor: 'rgba(0,0,0,0.05)', padding: 6, fontWeight: 'bold', color: '#666' },
item: { padding: 12, borderBottomWidth: 0.5, borderColor: '#eee' },
modalFooter: { flexDirection: 'row', gap: 10, marginTop: 25 },
btnModal: { flex: 1, padding: 15, borderRadius: 12, alignItems: 'center' },
confirmBox: { borderWidth: 1, borderRadius: 12, padding: 15, backgroundColor: 'rgba(0,0,0,0.02)' },
confirmTitle: { fontSize: 10, fontWeight: '900', color: '#0d6efd', marginBottom: 8, letterSpacing: 1 },
editInput: { borderBottomWidth: 1, paddingVertical: 5, fontSize: 14, borderColor: '#eee' },
horarioRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 10 },
miniInput: { borderBottomWidth: 1, borderColor: '#eee', flex: 1, padding: 4, fontSize: 12, textAlign: 'center' },
badgeHoras: { backgroundColor: '#0d6efd', padding: 5, borderRadius: 6, alignSelf: 'flex-start' },
badgeText: { color: '#fff', fontSize: 11, fontWeight: 'bold' }
});

View File

@@ -1,9 +1,11 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
Platform,
Platform // Importado para detetar o sistema operativo
,
SafeAreaView,
ScrollView,
StatusBar,
@@ -11,147 +13,190 @@ import {
Text,
TextInput,
TouchableOpacity,
View,
View
} from 'react-native';
import { useTheme } from '../../themecontext'; // Contexto global do tema
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
interface PerfilData {
id: string;
nome: string;
email: string;
n_escola: string;
telefone: string;
residencia: string;
tipo: string;
area: string;
idade?: number;
}
export default function PerfilProfessor() {
const router = useRouter();
const { isDarkMode } = useTheme(); // pega do contexto global
const { isDarkMode } = useTheme();
const [editando, setEditando] = useState(false);
const [loading, setLoading] = useState(true);
const [perfil, setPerfil] = useState<PerfilData | null>(null);
// Cores dinamicas baseadas no tema
const cores = {
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#212529',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
inputBg: isDarkMode ? '#1e1e1e' : '#f8f9fa',
inputBg: isDarkMode ? '#2c2c2c' : '#f8f9fa',
border: isDarkMode ? '#343a40' : '#ced4da',
azul: '#0d6efd',
vermelho: '#dc3545',
};
const [perfil, setPerfil] = useState({
nome: 'João Miranda',
email: 'joao.miranda@epvc.pt',
mecanografico: 'PROF-174',
area: 'Informática',
turmas: '12ºINF | 11ºINF | 10ºINF',
funcao: 'Orientador de Estágio',
});
useEffect(() => {
carregarPerfil();
}, []);
async function carregarPerfil() {
try {
setLoading(true);
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (error) throw error;
setPerfil(data);
}
} catch (error: any) {
Alert.alert('Erro', 'Não foi possível carregar os dados do perfil.');
} finally {
setLoading(false);
}
}
const guardarPerfil = async () => {
if (!perfil) return;
try {
const { error } = await supabase
.from('profiles')
.update({
nome: perfil.nome,
telefone: perfil.telefone,
residencia: perfil.residencia,
n_escola: perfil.n_escola,
area: perfil.area
})
.eq('id', perfil.id);
if (error) throw error;
setEditando(false);
Alert.alert('Sucesso', 'Perfil atualizado!');
} catch (error: any) {
Alert.alert('Erro', error.message);
}
};
const terminarSessao = async () => {
await supabase.auth.signOut();
router.replace('/');
};
const guardarPerfil = () => {
setEditando(false);
Alert.alert('Sucesso', 'Perfil atualizado com sucesso!');
// depois liga ao Supabase update
};
if (loading) {
return (
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
);
}
return (
<SafeAreaView
style={[
styles.container,
{ backgroundColor: cores.fundo, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
]}
>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={cores.fundo} />
<ScrollView contentContainerStyle={styles.content}>
{/* TOPO COM VOLTAR */}
// AJUSTE DE SAFE AREA AQUI: paddingTop dinâmico para Android e iOS
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor="transparent"
translucent
/>
<ScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{/* TOPO COM ESPAÇAMENTO PARA NOTIFICAÇÕES */}
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.push('/Professor/ProfessorHome')}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="arrow-back-outline" size={26} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>Perfil</Text>
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
<View style={{ width: 26 }} />
</View>
{/* HEADER */}
{/* HEADER PERFIL */}
<View style={styles.header}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Ionicons name="person" size={48} color="#fff" />
</View>
<Text style={[styles.name, { color: cores.texto }]}>{perfil.nome}</Text>
<Text style={[styles.role, { color: cores.textoSecundario }]}>{perfil.funcao}</Text>
<Text style={[styles.name, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.role, { color: cores.textoSecundario }]}>
{perfil?.area || 'Professor Orientador'}
</Text>
</View>
{/* INFORMAÇÕES */}
{/* CAMPOS DE INFORMAÇÃO */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<InfoField
label="Nome"
value={perfil.nome}
label="Nome Completo"
value={perfil?.nome || ''}
editable={editando}
onChange={(v: string) => setPerfil({ ...perfil, nome: v })}
cores={cores}
/>
<InfoField label="Email" value={perfil.email} editable={false} cores={cores} />
<InfoField
label="Nº Mecanográfico"
value={perfil.mecanografico}
editable={editando}
onChange={(v: string) => setPerfil({ ...perfil, mecanografico: v })}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)}
cores={cores}
/>
<InfoField
label="Área"
value={perfil.area}
label="Área / Departamento"
value={perfil?.area || ''}
editable={editando}
onChange={(v: string) => setPerfil({ ...perfil, area: v })}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, area: v } : null)}
cores={cores}
/>
<InfoField label="Email" value={perfil?.email || ''} editable={false} cores={cores} />
<InfoField
label="Nº Escola"
value={perfil?.n_escola || ''}
editable={editando}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)}
cores={cores}
/>
<InfoField
label="Turmas"
value={perfil.turmas}
label="Telefone"
value={perfil?.telefone || ''}
editable={editando}
onChange={(v: string) => setPerfil({ ...perfil, turmas: v })}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)}
cores={cores}
/>
</View>
{/* AÇÕES */}
{/* BOTÕES DE AÇÃO */}
<View style={styles.actions}>
{editando ? (
<TouchableOpacity style={[styles.primaryButton, { backgroundColor: cores.azul }]} onPress={guardarPerfil}>
<Ionicons name="save-outline" size={20} color="#fff" />
<Text style={styles.primaryText}>Guardar alterações</Text>
<Text style={styles.primaryText}>Guardar Alterações</Text>
</TouchableOpacity>
) : (
<ActionButton icon="create-outline" text="Editar perfil" onPress={() => setEditando(true)} cores={cores} />
<TouchableOpacity style={[styles.actionButton, { backgroundColor: cores.card }]} onPress={() => setEditando(true)}>
<Ionicons name="create-outline" size={20} color={cores.azul} />
<Text style={[styles.actionText, { color: cores.texto }]}>Editar Perfil</Text>
</TouchableOpacity>
)}
<ActionButton
icon="key-outline"
text="Alterar palavra-passe"
onPress={() => router.push('/Professor/redefenirsenha2')}
cores={cores}
/>
<ActionButton icon="log-out-outline" text="Terminar sessão" danger onPress={terminarSessao} cores={cores} />
<TouchableOpacity style={[styles.actionButton, { backgroundColor: cores.card }]} onPress={terminarSessao}>
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
<Text style={[styles.actionText, { color: cores.vermelho }]}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
/* COMPONENTES */
function InfoField({
label,
value,
editable,
onChange,
cores,
}: {
label: string;
value: string;
editable: boolean;
onChange?: (v: string) => void;
cores: any;
}) {
function InfoField({ label, value, editable, onChange, cores }: any) {
return (
<View style={styles.infoField}>
<Text style={[styles.label, { color: cores.textoSecundario }]}>{label}</Text>
@@ -162,71 +207,41 @@ function InfoField({
style={[styles.input, { backgroundColor: cores.inputBg, color: cores.texto, borderColor: cores.border }]}
/>
) : (
<Text style={[styles.value, { color: cores.texto }]}>{value}</Text>
<Text style={[styles.value, { color: cores.texto }]}>{value || 'Não definido'}</Text>
)}
</View>
);
}
function ActionButton({
icon,
text,
onPress,
danger = false,
cores,
}: {
icon: any;
text: string;
onPress: () => void;
danger?: boolean;
cores: any;
}) {
return (
<TouchableOpacity
style={[
styles.actionButton,
{ backgroundColor: cores.card },
danger && { borderWidth: 1, borderColor: cores.vermelho },
]}
onPress={onPress}
>
<Ionicons name={icon} size={20} color={danger ? cores.vermelho : cores.azul} />
<Text
style={[
styles.actionText,
danger && { color: cores.vermelho },
!danger && cores.texto === '#fff' && { color: '#fff' }, // texto branco no modo escuro
]}
>
{text}
</Text>
</TouchableOpacity>
);
}
/* ESTILOS */
const styles = StyleSheet.create({
container: { flex: 1 },
content: { padding: 24 },
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 },
// Estilo principal da Safe Area
safeArea: {
flex: 1,
// No Android, a barra de notificações não é respeitada automaticamente pelo SafeAreaView
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
},
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
content: { padding: 24, paddingBottom: 40 },
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 20,
marginTop: 10 // Margem extra de segurança
},
topTitle: { fontSize: 18, fontWeight: '700' },
header: { alignItems: 'center', marginBottom: 32 },
avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', marginBottom: 12 },
name: { fontSize: 22, fontWeight: '800' },
role: { fontSize: 14, marginTop: 4 },
card: { borderRadius: 18, padding: 20, marginBottom: 24, elevation: 4 },
infoField: { marginBottom: 16 },
label: { fontSize: 12, marginBottom: 4 },
value: { fontSize: 15, fontWeight: '600' },
input: { borderWidth: 1, borderRadius: 10, padding: 10, fontSize: 15 },
label: { fontSize: 11, marginBottom: 4, textTransform: 'uppercase', fontWeight: '600' },
value: { fontSize: 16, fontWeight: '600' },
input: { borderWidth: 1, borderRadius: 10, padding: 12, fontSize: 15 },
actions: { gap: 12 },
actionButton: { flexDirection: 'row', alignItems: 'center', borderRadius: 14, padding: 16 },
actionText: { fontSize: 15, fontWeight: '600', marginLeft: 12 },
primaryButton: { borderRadius: 14, padding: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
primaryText: { color: '#fff', fontWeight: '700', marginLeft: 8 },
});
});

View File

@@ -16,51 +16,44 @@ export default function LoginScreen() {
const handleLoginSuccess = async () => {
try {
// 1⃣ Obter utilizador autenticado
// Buscar utilizador autenticado
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (userError || !user) {
console.log('Erro ao obter utilizador:', userError);
Alert.alert('Erro', 'Utilizador não autenticado');
return;
}
console.log('Utilizador autenticado:', user.id);
// 2⃣ Buscar tipo do utilizador
// Buscar perfil
const { data, error } = await supabase
.from('profiles')
.select('tipo')
.eq('id', user.id)
.single();
.maybeSingle();
if (error) {
console.log('Erro ao buscar perfil:', error);
Alert.alert('Erro', error.message);
return;
}
if (!data) {
console.log('Perfil não encontrado');
Alert.alert('Erro', 'Perfil não encontrado');
return;
}
console.log('Tipo de utilizador:', data.tipo);
// 3⃣ Redirecionar conforme o tipo
// Redirecionar
if (data.tipo === 'professor') {
router.replace('/Professor/ProfessorHome');
} else if (data.tipo === 'aluno') {
router.replace('/Aluno/AlunoHome');
} else {
Alert.alert('Erro', 'Tipo de utilizador inválido');
Alert.alert('Erro', 'Tipo inválido');
}
} catch (err) {
console.error('Erro inesperado:', err);
Alert.alert('Erro', 'Erro inesperado no login');
}
};
@@ -82,7 +75,6 @@ export default function LoginScreen() {
</Text>
</View>
{/* COMPONENTE DE LOGIN */}
<Auth onLoginSuccess={handleLoginSuccess} />
</View>
</ScrollView>
@@ -116,4 +108,4 @@ const styles = StyleSheet.create({
color: '#636e72',
textAlign: 'center',
},
});
});