atualizações - noite

This commit is contained in:
2026-03-11 00:08:38 +00:00
parent c501fa7801
commit f468c926e7
6 changed files with 642 additions and 409 deletions

View File

@@ -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' },
});

View File

@@ -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 }]}> 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' },
});

View File

@@ -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 },
});

View File

@@ -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 },

View File

@@ -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',
},
});
});