atualizações - noite
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
// app/Professor/Alunos/DetalhesAluno.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
@@ -14,7 +14,6 @@ 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;
|
||||
@@ -24,6 +23,13 @@ interface AlunoEstado {
|
||||
telefone: string;
|
||||
residencia: string;
|
||||
idade: string;
|
||||
empresa_nome: string;
|
||||
tutor_nome: string;
|
||||
tutor_tel: string;
|
||||
data_inicio: string;
|
||||
data_fim: string;
|
||||
horas_diarias: string;
|
||||
horarios_detalhados: string[];
|
||||
}
|
||||
|
||||
const DetalhesAlunos = memo(() => {
|
||||
@@ -31,130 +37,184 @@ const DetalhesAlunos = memo(() => {
|
||||
const params = useLocalSearchParams();
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
const colors = {
|
||||
background: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#ffffff',
|
||||
text: isDarkMode ? '#ffffff' : '#000000',
|
||||
label: isDarkMode ? '#aaaaaa' : '#6c757d',
|
||||
};
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const alunoId =
|
||||
typeof params.alunoId === 'string'
|
||||
? params.alunoId
|
||||
: Array.isArray(params.alunoId)
|
||||
? params.alunoId[0]
|
||||
: null;
|
||||
const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null;
|
||||
|
||||
const [aluno, setAluno] = useState<AlunoEstado | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!alunoId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
fetchAluno();
|
||||
if (alunoId) fetchAluno();
|
||||
}, [alunoId]);
|
||||
|
||||
const fetchAluno = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('alunos')
|
||||
.select(`
|
||||
id,
|
||||
nome,
|
||||
n_escola,
|
||||
turma_curso,
|
||||
profiles!alunos_profile_id_fkey (
|
||||
email,
|
||||
telefone,
|
||||
residencia,
|
||||
idade
|
||||
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 )
|
||||
)
|
||||
`)
|
||||
.eq('id', alunoId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.log('Erro ao buscar:', error.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (error) throw error;
|
||||
|
||||
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;
|
||||
const d = data as any;
|
||||
const perfil = d.profiles;
|
||||
const estagio = Array.isArray(d.estagios) ? d.estagios[0] : d.estagios;
|
||||
const empresa = estagio?.empresas;
|
||||
|
||||
let listaHorarios: string[] = [];
|
||||
if (estagio?.id) {
|
||||
const { data: hData } = await supabase
|
||||
.from('horarios_estagio')
|
||||
.select('hora_inicio, hora_fim')
|
||||
.eq('estagio_id', estagio.id);
|
||||
|
||||
if (hData) {
|
||||
listaHorarios = hData.map(h =>
|
||||
`${h.hora_inicio.substring(0,5)} até às ${h.hora_fim.substring(0,5)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setAluno({
|
||||
id: String(data.id),
|
||||
nome: data.nome || 'Sem nome',
|
||||
n_escola: String(data.n_escola || '-'),
|
||||
turma_curso: data.turma_curso || '-',
|
||||
id: String(d.id),
|
||||
nome: d.nome || 'Sem nome',
|
||||
n_escola: String(d.n_escola || '-'),
|
||||
turma_curso: d.turma_curso || '-',
|
||||
email: perfil?.email ?? '-',
|
||||
telefone: perfil?.telefone ?? '-',
|
||||
residencia: perfil?.residencia ?? '-',
|
||||
idade: perfil?.idade ? String(perfil.idade) : '-',
|
||||
empresa_nome: empresa?.nome || 'Não atribuída',
|
||||
tutor_nome: empresa?.tutor_nome || 'Não definido',
|
||||
tutor_tel: empresa?.tutor_telefone || '-',
|
||||
data_inicio: estagio?.data_inicio || '-',
|
||||
data_fim: estagio?.data_fim || '-',
|
||||
horas_diarias: estagio?.horas_diarias || '0h',
|
||||
horarios_detalhados: listaHorarios || [], // Garantia de array vazio
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.log('Erro inesperado:', err);
|
||||
} catch (err: any) {
|
||||
console.log('Erro:', err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: colors.background }]}>
|
||||
<Text style={[styles.center, { color: colors.text }]}>A carregar...</Text>
|
||||
</SafeAreaView>
|
||||
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Verificação de segurança extra para o objeto aluno
|
||||
if (!aluno) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: colors.background }]}>
|
||||
<Text style={[styles.center, { color: colors.text }]}>Aluno não encontrado</Text>
|
||||
</SafeAreaView>
|
||||
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
|
||||
<Text style={{ color: cores.texto }}>Dados indisponíveis</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: colors.background }]} edges={['top', 'bottom']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={colors.background} />
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]} edges={['top']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: cores.card }]}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.titulo, { color: colors.text }]}>{aluno.nome}</Text>
|
||||
|
||||
<View style={{ width: 24 }} />
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<View style={[styles.card, { backgroundColor: colors.card }]}>
|
||||
{renderCampo('Número Escola', aluno.n_escola, colors)}
|
||||
{renderCampo('Turma', aluno.turma_curso, colors)}
|
||||
{renderCampo('Email', aluno.email, colors)}
|
||||
{renderCampo('Telefone', aluno.telefone, colors)}
|
||||
{renderCampo('Residência', aluno.residencia, colors)}
|
||||
{renderCampo('Idade', aluno.idade, colors)}
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={styles.profileSection}>
|
||||
<View style={[styles.mainAvatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarLetter, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<Text style={[styles.mainName, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.badgeText, { color: cores.azul }]}>{aluno.turma_curso}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Pessoais</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
<InfoRow icon="school-outline" label="Nº Escola" valor={aluno.n_escola} cores={cores} />
|
||||
<InfoRow icon="mail-outline" label="Email" valor={aluno.email} cores={cores} />
|
||||
<InfoRow icon="call-outline" label="Telefone" valor={aluno.telefone} cores={cores} />
|
||||
<InfoRow icon="location-outline" label="Residência" valor={aluno.residencia} cores={cores} ultimo />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Informação de Estágio</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
<InfoRow icon="business-outline" label="Empresa" valor={aluno.empresa_nome} cores={cores} />
|
||||
<InfoRow icon="person-outline" label="Tutor / Responsável" valor={aluno.tutor_nome} cores={cores} />
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="time-outline" size={18} color={cores.azul} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.infoLabel, { color: cores.secundario }]}>Horários Registados</Text>
|
||||
{/* O USO DE OPTIONAL CHAINING AQUI EVITA O ERRO */}
|
||||
{(aluno.horarios_detalhados?.length ?? 0) > 0 ? (
|
||||
aluno.horarios_detalhados.map((h, index) => (
|
||||
<Text key={index} style={[styles.infoValor, { color: cores.texto, marginBottom: 2 }]}>{h}</Text>
|
||||
))
|
||||
) : (
|
||||
<Text style={[styles.infoValor, { color: cores.texto }]}>Sem horários definidos</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', borderTopWidth: 1, borderTopColor: cores.borda }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<InfoRow icon="calendar-outline" label="Início" valor={aluno.data_inicio} cores={cores} ultimo />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<InfoRow icon="calendar-outline" label="Previsão Fim" valor={aluno.data_fim} cores={cores} ultimo />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
});
|
||||
|
||||
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>
|
||||
const InfoRow = ({ icon, label, valor, cores, ultimo }: any) => (
|
||||
<View style={[styles.infoRow, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.infoLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<Text style={[styles.infoValor, { color: cores.texto }]} numberOfLines={1}>{valor}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -162,25 +222,21 @@ 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
|
||||
},
|
||||
titulo: { fontSize: 20, fontWeight: 'bold' },
|
||||
container: { padding: 16 },
|
||||
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' },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
backBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
headerTitle: { fontSize: 18, fontWeight: '800' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
profileSection: { alignItems: 'center', marginBottom: 25 },
|
||||
mainAvatar: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
||||
avatarLetter: { fontSize: 28, fontWeight: '800' },
|
||||
mainName: { fontSize: 22, fontWeight: '800', textAlign: 'center' },
|
||||
badge: { marginTop: 6, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 20 },
|
||||
badgeText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase' },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginTop: 25, marginBottom: 10, marginLeft: 5 },
|
||||
card: { borderRadius: 20, padding: 10, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
infoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 10 },
|
||||
iconBox: { width: 36, height: 36, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
|
||||
infoLabel: { fontSize: 10, fontWeight: '600', textTransform: 'uppercase', marginBottom: 2 },
|
||||
infoValor: { fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
@@ -30,120 +31,120 @@ const ListaAlunosProfessor = memo(() => {
|
||||
const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const cores = {
|
||||
fundo: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#fff' : '#000',
|
||||
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
|
||||
azul: '#0d6efd',
|
||||
};
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlunos();
|
||||
}, []);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome, n_escola, ano, turma_curso')
|
||||
.order('ano', { ascending: false })
|
||||
.order('nome', { ascending: true });
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome, n_escola, ano, turma_curso')
|
||||
.order('ano', { ascending: false });
|
||||
if (error) throw error;
|
||||
|
||||
if (error) {
|
||||
console.error('Erro Supabase:', error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
setTurmas([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
console.log('Nenhum aluno encontrado');
|
||||
setTurmas([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Agrupar por ano + turma_curso
|
||||
const agrupadas: Record<string, Aluno[]> = {};
|
||||
|
||||
data.forEach(item => {
|
||||
const nomeTurma = `${item.ano}º ${item.turma_curso}`;
|
||||
if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = [];
|
||||
|
||||
agrupadas[nomeTurma].push({
|
||||
id: item.id,
|
||||
nome: item.nome,
|
||||
n_escola: item.n_escola,
|
||||
turma: nomeTurma,
|
||||
const agrupadas: Record<string, Aluno[]> = {};
|
||||
data.forEach(item => {
|
||||
const nomeTurma = `${item.ano}º ${item.turma_curso}`;
|
||||
if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = [];
|
||||
agrupadas[nomeTurma].push({
|
||||
id: item.id,
|
||||
nome: item.nome,
|
||||
n_escola: item.n_escola,
|
||||
turma: nomeTurma,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setTurmas(
|
||||
Object.keys(agrupadas).map(nome => ({
|
||||
nome,
|
||||
alunos: agrupadas[nome],
|
||||
}))
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
setTurmas(
|
||||
Object.keys(agrupadas).map(nome => ({
|
||||
nome,
|
||||
alunos: agrupadas[nome],
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar alunos:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTurmas = turmas
|
||||
.map(turma => ({
|
||||
...turma,
|
||||
alunos: turma.alunos.filter(a =>
|
||||
a.nome.toLowerCase().includes(search.toLowerCase())
|
||||
a.nome.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.n_escola.includes(search)
|
||||
),
|
||||
}))
|
||||
.filter(t => t.alunos.length > 0);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
{/* HEADER FIXO ESTILO PREMIUM */}
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Alunos</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.tituloGeral, { color: cores.texto }]}>
|
||||
Alunos
|
||||
</Text>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.secundario} />
|
||||
<TextInput
|
||||
placeholder="Procurar aluno ou Nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
style={[styles.search, { backgroundColor: cores.card, color: cores.texto }]}
|
||||
placeholder="Pesquisar aluno..."
|
||||
placeholderTextColor={cores.textoSecundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
{loading ? (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.section}>
|
||||
{/* BADGE DA TURMA */}
|
||||
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.turmaLabel, { color: cores.azul }]}>
|
||||
{item.nome} • {item.alunos.length} Alunos
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{loading && (
|
||||
<Text style={{ textAlign: 'center', marginVertical: 20, color: cores.texto }}>
|
||||
A carregar alunos...
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
renderItem={({ item }) => (
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
<Text style={[styles.turmaNome, { color: cores.azul }]}>
|
||||
{item.nome} ({item.alunos.length})
|
||||
</Text>
|
||||
|
||||
<View style={styles.listaAlunos}>
|
||||
{item.alunos.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
style={styles.alunoItem}
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/Professor/Alunos/DetalhesAluno',
|
||||
@@ -151,15 +152,24 @@ const ListaAlunosProfessor = memo(() => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>
|
||||
{aluno.n_escola} – {aluno.nome}
|
||||
</Text>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>
|
||||
{aluno.nome.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>Nº Escola: {aluno.n_escola}</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
});
|
||||
@@ -167,15 +177,69 @@ const ListaAlunosProfessor = memo(() => {
|
||||
export default ListaAlunosProfessor;
|
||||
|
||||
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 },
|
||||
tituloGeral: { fontSize: 22, fontWeight: 'bold' },
|
||||
spacer: { width: 40 },
|
||||
search: { borderRadius: 10, padding: 10, margin: 10 },
|
||||
card: { borderRadius: 10, padding: 15, marginHorizontal: 10, marginBottom: 10, elevation: 2 },
|
||||
turmaNome: { fontSize: 18, fontWeight: 'bold' },
|
||||
listaAlunos: { marginTop: 10, paddingLeft: 10 },
|
||||
alunoItem: { paddingVertical: 5 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '600' },
|
||||
});
|
||||
safe: {
|
||||
flex: 1,
|
||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0,
|
||||
},
|
||||
headerFixed: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: 60,
|
||||
},
|
||||
backBtn: { width: 40, height: 40, justifyContent: 'center' },
|
||||
title: { fontSize: 24, fontWeight: '800' },
|
||||
searchBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: 15,
|
||||
paddingHorizontal: 15,
|
||||
height: 48,
|
||||
marginTop: 5,
|
||||
},
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15 },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
|
||||
section: { marginBottom: 25 },
|
||||
turmaBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 10,
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
turmaLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5
|
||||
},
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 14,
|
||||
borderRadius: 20,
|
||||
marginBottom: 10,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
},
|
||||
avatar: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatarText: { fontSize: 17, fontWeight: '700' },
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
nome: { fontSize: 16, fontWeight: '700' },
|
||||
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
@@ -13,59 +14,78 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
interface Aluno {
|
||||
id: number;
|
||||
id: string;
|
||||
nome: string;
|
||||
turma: string;
|
||||
}
|
||||
|
||||
const alunosData: Aluno[] = [
|
||||
{ id: 1, nome: 'João Silva', turma: '12ºINF' },
|
||||
{ id: 2, nome: 'Maria Fernandes', turma: '12ºINF' },
|
||||
{ id: 3, nome: 'Pedro Costa', turma: '11ºINF' },
|
||||
];
|
||||
|
||||
export default function Presencas() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const [pesquisa, setPesquisa] = useState('');
|
||||
const [alunos, setAlunos] = useState<Aluno[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const cores = useMemo(
|
||||
() => ({
|
||||
fundo: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#fff' : '#212529',
|
||||
secundario: isDarkMode ? '#adb5bd' : '#6c757d',
|
||||
azul: '#0d6efd',
|
||||
input: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
borda: isDarkMode ? '#333' : '#dee2e6',
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}),
|
||||
[isDarkMode]
|
||||
);
|
||||
|
||||
const alunosFiltrados = alunosData.filter(a =>
|
||||
useEffect(() => {
|
||||
fetchAlunos();
|
||||
}, []);
|
||||
|
||||
async function fetchAlunos() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Busca apenas id e nome, filtrando por tipo aluno
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, nome')
|
||||
.eq('tipo', 'aluno')
|
||||
.order('nome', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (data) {
|
||||
setAlunos(data as Aluno[]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao carregar alunos:", error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const alunosFiltrados = alunos.filter(a =>
|
||||
a.nome.toLowerCase().includes(pesquisa.toLowerCase())
|
||||
);
|
||||
|
||||
const turmas = Array.from(new Set(alunosFiltrados.map(a => a.turma)));
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
{/* HEADER */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={26} color={cores.texto} />
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Presenças</Text>
|
||||
<View style={{ width: 26 }} />
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
{/* PESQUISA */}
|
||||
<View style={[styles.searchBox, { backgroundColor: cores.input, borderColor: cores.borda }]}>
|
||||
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.secundario} />
|
||||
<TextInput
|
||||
placeholder="Pesquisar aluno..."
|
||||
@@ -75,33 +95,47 @@ export default function Presencas() {
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* TURMAS */}
|
||||
{turmas.map(turma => (
|
||||
<View key={turma}>
|
||||
<Text style={[styles.turma, { color: cores.azul }]}>{turma}</Text>
|
||||
|
||||
{alunosFiltrados
|
||||
.filter(a => a.turma === turma)
|
||||
.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/Professor/Alunos/CalendarioPresencas',
|
||||
params: { alunoId: aluno.id, nome: aluno.nome },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Ionicons name="person-outline" size={24} color={cores.azul} />
|
||||
{loading ? (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
{alunosFiltrados.length === 0 ? (
|
||||
<View style={styles.empty}>
|
||||
<Text style={{ color: cores.secundario }}>Nenhum aluno encontrado.</Text>
|
||||
</View>
|
||||
) : (
|
||||
alunosFiltrados.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/Professor/Alunos/CalendarioPresencas',
|
||||
params: { alunoId: aluno.id, nome: aluno.nome },
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>
|
||||
{aluno.nome.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Ionicons name="chevron-forward" size={20} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>Ver registo de presenças</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -109,32 +143,53 @@ export default function Presencas() {
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
|
||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0,
|
||||
},
|
||||
container: { padding: 20 },
|
||||
header: {
|
||||
headerFixed: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
height: 60,
|
||||
},
|
||||
title: { fontSize: 24, fontWeight: '700' },
|
||||
backBtn: { width: 40, height: 40, justifyContent: 'center' },
|
||||
title: { fontSize: 22, fontWeight: '800' },
|
||||
searchBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 20,
|
||||
borderRadius: 15,
|
||||
paddingHorizontal: 15,
|
||||
height: 48,
|
||||
},
|
||||
searchInput: { flex: 1, marginLeft: 8, paddingVertical: 10 },
|
||||
turma: { fontSize: 16, fontWeight: '700', marginBottom: 10 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15 },
|
||||
scrollContent: { padding: 20 },
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 14,
|
||||
marginBottom: 12,
|
||||
padding: 14,
|
||||
borderRadius: 18,
|
||||
marginBottom: 10,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 5,
|
||||
},
|
||||
nome: { flex: 1, marginLeft: 12, fontSize: 16, fontWeight: '600' },
|
||||
});
|
||||
avatar: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatarText: { fontSize: 16, fontWeight: '700' },
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
nome: { fontSize: 16, fontWeight: '700' },
|
||||
subText: { fontSize: 12, marginTop: 2 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
empty: { alignItems: 'center', marginTop: 40 },
|
||||
});
|
||||
@@ -4,8 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform // Importado para detetar o sistema operativo
|
||||
,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
@@ -26,7 +25,7 @@ interface PerfilData {
|
||||
telefone: string;
|
||||
residencia: string;
|
||||
tipo: string;
|
||||
area: string;
|
||||
curso: string; // Sincronizado com a coluna que criaste no Supabase
|
||||
idade?: number;
|
||||
}
|
||||
|
||||
@@ -82,14 +81,16 @@ export default function PerfilProfessor() {
|
||||
telefone: perfil.telefone,
|
||||
residencia: perfil.residencia,
|
||||
n_escola: perfil.n_escola,
|
||||
area: perfil.area
|
||||
curso: perfil.curso // Usando o nome correto da coluna
|
||||
})
|
||||
.eq('id', perfil.id);
|
||||
|
||||
if (error) throw error;
|
||||
setEditando(false);
|
||||
Alert.alert('Sucesso', 'Perfil atualizado!');
|
||||
Alert.alert('Sucesso', 'Perfil atualizado com sucesso!');
|
||||
} catch (error: any) {
|
||||
Alert.alert('Erro', error.message);
|
||||
// Se der erro aqui, vai dar merda porque o nome da coluna pode estar mal escrito no Supabase
|
||||
Alert.alert('Erro ao gravar', 'Verifica se a coluna se chama exatamente "curso". ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,7 +108,6 @@ export default function PerfilProfessor() {
|
||||
}
|
||||
|
||||
return (
|
||||
// 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'}
|
||||
@@ -119,7 +119,6 @@ export default function PerfilProfessor() {
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* TOPO COM ESPAÇAMENTO PARA NOTIFICAÇÕES */}
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back-outline" size={26} color={cores.texto} />
|
||||
@@ -128,18 +127,16 @@ export default function PerfilProfessor() {
|
||||
<View style={{ width: 26 }} />
|
||||
</View>
|
||||
|
||||
{/* 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?.area || 'Professor Orientador'}
|
||||
{perfil?.curso || 'Sem curso definido'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* CAMPOS DE INFORMAÇÃO */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
<InfoField
|
||||
label="Nome Completo"
|
||||
@@ -148,14 +145,17 @@ export default function PerfilProfessor() {
|
||||
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)}
|
||||
cores={cores}
|
||||
/>
|
||||
|
||||
<InfoField
|
||||
label="Área / Departamento"
|
||||
value={perfil?.area || ''}
|
||||
label="Área / Curso"
|
||||
value={perfil?.curso || ''}
|
||||
editable={editando}
|
||||
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, area: v } : null)}
|
||||
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)}
|
||||
cores={cores}
|
||||
/>
|
||||
<InfoField label="Email" value={perfil?.email || ''} editable={false} cores={cores} />
|
||||
|
||||
<InfoField label="Email (Login)" value={perfil?.email || ''} editable={false} cores={cores} />
|
||||
|
||||
<InfoField
|
||||
label="Nº Escola"
|
||||
value={perfil?.n_escola || ''}
|
||||
@@ -163,6 +163,7 @@ export default function PerfilProfessor() {
|
||||
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)}
|
||||
cores={cores}
|
||||
/>
|
||||
|
||||
<InfoField
|
||||
label="Telefone"
|
||||
value={perfil?.telefone || ''}
|
||||
@@ -172,7 +173,6 @@ export default function PerfilProfessor() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* BOTÕES DE AÇÃO */}
|
||||
<View style={styles.actions}>
|
||||
{editando ? (
|
||||
<TouchableOpacity style={[styles.primaryButton, { backgroundColor: cores.azul }]} onPress={guardarPerfil}>
|
||||
@@ -186,6 +186,14 @@ export default function PerfilProfessor() {
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor: cores.card }]}
|
||||
onPress={() => router.push('/Professor/redefenirsenha2')}
|
||||
>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
|
||||
<Text style={[styles.actionText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<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>
|
||||
@@ -214,21 +222,13 @@ function InfoField({ label, value, editable, onChange, cores }: any) {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// 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
|
||||
},
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, marginTop: 10 },
|
||||
topTitle: { fontSize: 18, fontWeight: '700' },
|
||||
header: { alignItems: 'center', marginBottom: 32 },
|
||||
avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', marginBottom: 12 },
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
@@ -10,38 +13,84 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../../themecontext'; // assumindo que tens o ThemeContext
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
// Tipagem para os ícones do Ionicons
|
||||
type IonIconName = keyof typeof Ionicons.glyphMap;
|
||||
|
||||
export default function ProfessorMenu() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const [nome, setNome] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const cores = {
|
||||
fundo: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#fff' : '#212529',
|
||||
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
|
||||
azul: '#0d6efd',
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function obterNome() {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('nome')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
// Alterado para carregar o nome completo sem divisões
|
||||
if (data?.nome) setNome(data.nome);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Erro ao carregar nome:", err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
obterNome();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
backgroundColor={cores.fundo}
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
|
||||
{/* HEADER */}
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* HEADER COM NOME COMPLETO */}
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.welcome, { color: cores.textoSecundario }]}>Olá,</Text>
|
||||
<Text style={[styles.name, { color: cores.texto }]}>Professor 👨🏫</Text>
|
||||
<Text style={[styles.subtitle, { color: cores.textoSecundario }]}>Gerencie os estágios facilmente</Text>
|
||||
<Text style={[styles.welcome, { color: cores.textoSecundario }]}>Bem-vindo,</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={cores.azul} style={{ alignSelf: 'flex-start', marginTop: 8 }} />
|
||||
) : (
|
||||
<Text style={[styles.name, { color: cores.texto }]}>
|
||||
{nome || 'Professor'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
|
||||
<View style={[styles.dot, { backgroundColor: cores.azul }]} />
|
||||
<Text style={[styles.subtitle, { color: cores.azul }]}>Painel de Gestão de Estágios</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* GRID DE OPÇÕES */}
|
||||
<View style={styles.grid}>
|
||||
|
||||
<MenuCard
|
||||
icon="person-outline"
|
||||
title="Perfil"
|
||||
@@ -61,7 +110,7 @@ export default function ProfessorMenu() {
|
||||
<MenuCard
|
||||
icon="document-text-outline"
|
||||
title="Sumários"
|
||||
subtitle="Verificar sumários"
|
||||
subtitle="Verificar registos"
|
||||
onPress={() => router.push('/Professor/Alunos/Sumarios')}
|
||||
cores={cores}
|
||||
/>
|
||||
@@ -85,7 +134,7 @@ export default function ProfessorMenu() {
|
||||
<MenuCard
|
||||
icon="briefcase-outline"
|
||||
title="Estágios"
|
||||
subtitle="Criar / Editar estágios"
|
||||
subtitle="Criar / Editar"
|
||||
onPress={() => router.push('/Professor/Alunos/Estagios')}
|
||||
cores={cores}
|
||||
/>
|
||||
@@ -105,33 +154,30 @@ export default function ProfessorMenu() {
|
||||
onPress={() => router.push('/Professor/Empresas/ListaEmpresas')}
|
||||
cores={cores}
|
||||
/>
|
||||
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
/* CARD REUTILIZÁVEL */
|
||||
function MenuCard({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
onPress,
|
||||
cores,
|
||||
}: {
|
||||
icon: any;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
onPress: () => void;
|
||||
cores: any;
|
||||
function MenuCard({ icon, title, subtitle, onPress, cores }: {
|
||||
icon: IonIconName;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
onPress: () => void;
|
||||
cores: any
|
||||
}) {
|
||||
return (
|
||||
<TouchableOpacity style={[styles.card, { backgroundColor: cores.card }]} onPress={onPress}>
|
||||
<Ionicons name={icon} size={28} color={cores.azul} />
|
||||
<Text style={[styles.cardTitle, { color: cores.texto }]}>{title}</Text>
|
||||
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]}>{subtitle}</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={24} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.cardTitle, { color: cores.texto }]} numberOfLines={1}>{title}</Text>
|
||||
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]} numberOfLines={1}>{subtitle}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -139,27 +185,43 @@ function MenuCard({
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
|
||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 10 : 10,
|
||||
},
|
||||
content: {
|
||||
padding: 24,
|
||||
padding: 20,
|
||||
},
|
||||
header: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
welcome: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
name: {
|
||||
fontSize: 28,
|
||||
fontSize: 26, // Reduzi ligeiramente o tamanho para nomes completos não quebrarem
|
||||
fontWeight: '800',
|
||||
marginTop: 4,
|
||||
letterSpacing: -0.5,
|
||||
marginBottom: 12,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
},
|
||||
dot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
marginRight: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
marginTop: 6,
|
||||
fontWeight: '400',
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
@@ -167,24 +229,31 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
card: {
|
||||
width: '48%',
|
||||
borderRadius: 18,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
alignItems: 'flex-start',
|
||||
width: (width - 55) / 2,
|
||||
borderRadius: 24,
|
||||
padding: 18,
|
||||
marginBottom: 15,
|
||||
elevation: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 10,
|
||||
elevation: 4,
|
||||
},
|
||||
iconBox: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 14,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
marginTop: 12,
|
||||
},
|
||||
cardSubtitle: {
|
||||
fontSize: 13,
|
||||
marginTop: 4,
|
||||
fontSize: 12,
|
||||
marginTop: 3,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user