This commit is contained in:
2026-05-04 15:03:11 +01:00
parent ac6c287cd4
commit 0a57a3d8ba
3 changed files with 285 additions and 9 deletions

View File

@@ -0,0 +1,274 @@
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../../lib/supabase';
import { useTheme } from '../../themecontext';
export default function EmpresaHome() {
const { isDarkMode } = useTheme();
const router = useRouter();
const [pendentes, setPendentes] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [empresaNome, setEmpresaNome] = useState('');
const themeStyles = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: '#2390a6',
laranja: '#dd8707',
verde: '#10B981',
vermelho: '#EF4444',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
inputFundo: isDarkMode ? '#252525' : '#F1F5F9',
}), [isDarkMode]);
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
if (!isManualRefresh) setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
// 1. Identificar quem é a empresa que tem o login feito
const { data: empresa } = await supabase
.from('empresas')
.select('id, nome')
.eq('user_id', user.id)
.single();
if (!empresa) {
setPendentes([]);
return;
}
setEmpresaNome(empresa.nome);
// 2. Buscar todos os estágios ligados a esta empresa
const { data: estagios } = await supabase
.from('estagios')
.select('aluno_id')
.eq('empresa_id', empresa.id);
if (!estagios || estagios.length === 0) {
setPendentes([]);
return;
}
const alunoIds = estagios.map(e => e.aluno_id);
// 3. Buscar os nomes dos alunos (para o tutor saber quem está a avaliar)
const { data: alunos } = await supabase
.from('alunos')
.select('id, nome')
.in('id', alunoIds);
const mapaAlunos: Record<string, string> = {};
alunos?.forEach(a => { mapaAlunos[a.id] = a.nome; });
// 4. Buscar apenas as presenças que estão PENDENTES para estes alunos
const { data: presencas } = await supabase
.from('presencas')
.select('*')
.in('aluno_id', alunoIds)
.eq('estado', 'presente')
.eq('estado_tutor', 'pendente')
.order('data', { ascending: false });
const listaFormatada = presencas?.map(p => ({
...p,
aluno_nome: mapaAlunos[p.aluno_id] || 'Aluno Desconhecido'
})) || [];
setPendentes(listaFormatada);
} catch (error) {
console.error(error);
Alert.alert("Erro", "Falha ao carregar as validações pendentes.");
} finally {
if (!isManualRefresh) setLoading(false);
setRefreshing(false);
}
};
// Atualiza sempre que o ecrã ganha foco
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchValidaçõesPendentes(true);
}, []);
// 🟢 FUNÇÃO PARA APROVAR OU REJEITAR
const lidarComPresenca = async (aluno_id: string, data: string, decisao: 'aprovado' | 'rejeitado') => {
try {
const { error } = await supabase
.from('presencas')
.update({ estado_tutor: decisao })
.match({ aluno_id, data }); // Dá match exato ao aluno e àquele dia
if (error) throw error;
// Avisa visualmente do sucesso e limpa aquele cartão da lista
if (decisao === 'aprovado') {
Alert.alert("✅ Validado", "Horas e sumário aprovados com sucesso!");
} else {
Alert.alert("❌ Rejeitado", "O registo foi devolvido ao aluno para correção.");
}
// Atualiza a lista removendo o que acabou de ser processado
setPendentes(prev => prev.filter(p => !(p.aluno_id === aluno_id && p.data === data)));
} catch (e: any) {
Alert.alert("Erro ao validar", e.message);
}
};
// Formatar data (AAAA-MM-DD -> DD/MM/AAAA)
const formatarData = (dataStr: string) => {
if (!dataStr) return '';
const parts = dataStr.split('-');
if (parts.length !== 3) return dataStr;
return `${parts[2]}/${parts[1]}/${parts[0]}`;
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.topBar}>
<View>
<Text style={[styles.greeting, { color: themeStyles.textoSecundario }]}>Painel da Entidade</Text>
<Text style={[styles.title, { color: themeStyles.texto }]}>{empresaNome || 'A carregar...'}</Text>
</View>
<TouchableOpacity style={[styles.logoutBtn, { borderColor: themeStyles.borda }]} onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}>
<Ionicons name="log-out-outline" size={24} color={themeStyles.vermelho} />
</TouchableOpacity>
</View>
<View style={styles.headerTitleContainer}>
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
<View style={[styles.badge, { backgroundColor: themeStyles.laranja + '20' }]}>
<Text style={[styles.badgeText, { color: themeStyles.laranja }]}>{pendentes.length}</Text>
</View>
</View>
{loading && !refreshing ? (
<View style={styles.centerBox}>
<ActivityIndicator size="large" color={themeStyles.azul} />
</View>
) : (
<ScrollView
contentContainerStyle={styles.scroll}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />}
>
{pendentes.length === 0 ? (
<View style={[styles.emptyBox, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}>
<Ionicons name="checkmark-done-circle" size={60} color={themeStyles.verde} style={{ marginBottom: 15 }} />
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Tudo em dia!</Text>
<Text style={[styles.emptyDesc, { color: themeStyles.textoSecundario }]}>
Não tens sumários ou presenças de alunos a aguardar a tua validação neste momento.
</Text>
</View>
) : (
pendentes.map((item, index) => (
<View key={index} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.cardHeader}>
<View style={{ flex: 1 }}>
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>
<Ionicons name="person" size={14} color={themeStyles.textoSecundario} /> {item.aluno_nome}
</Text>
<Text style={[styles.dataText, { color: themeStyles.azul }]}>
<Ionicons name="calendar-outline" size={12} /> {formatarData(item.data)}
</Text>
</View>
<View style={[styles.statusTag, { backgroundColor: themeStyles.laranja + '20' }]}>
<Text style={[styles.statusTagText, { color: themeStyles.laranja }]}>POR VALIDAR</Text>
</View>
</View>
<View style={[styles.sumarioBox, { backgroundColor: themeStyles.inputFundo }]}>
<Text style={[styles.sumarioLabel, { color: themeStyles.textoSecundario }]}>Sumário Submetido:</Text>
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
{item.sumario ? item.sumario : "O aluno não escreveu sumário para este dia."}
</Text>
</View>
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]}
onPress={() => lidarComPresenca(item.aluno_id, item.data, 'rejeitado')}
>
<Ionicons name="close" size={20} color={themeStyles.vermelho} />
<Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: themeStyles.verde }]}
onPress={() => lidarComPresenca(item.aluno_id, item.data, 'aprovado')}
>
<Ionicons name="checkmark" size={20} color="#fff" />
<Text style={[styles.btnActionText, { color: '#fff' }]}>Aprovar</Text>
</TouchableOpacity>
</View>
</View>
))
)}
</ScrollView>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 10 },
greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
title: { fontSize: 24, fontWeight: '900', marginTop: 2 },
logoutBtn: { width: 45, height: 45, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
headerTitleContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 15, gap: 10 },
sectionTitle: { fontSize: 18, fontWeight: '800' },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 },
badgeText: { fontSize: 12, fontWeight: '900' },
scroll: { paddingHorizontal: 20, paddingBottom: 40 },
emptyBox: { alignItems: 'center', padding: 40, borderRadius: 24, borderWidth: 1, borderStyle: 'dashed', marginTop: 30 },
emptyTitle: { fontSize: 20, fontWeight: '900', marginBottom: 8 },
emptyDesc: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '500' },
card: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 15 },
alunoName: { fontSize: 17, fontWeight: '900', marginBottom: 4 },
dataText: { fontSize: 13, fontWeight: '800' },
statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
sumarioBox: { padding: 15, borderRadius: 16, marginBottom: 15 },
sumarioLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6 },
sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 20 },
actionRow: { flexDirection: 'row', gap: 12 },
btnAction: { flex: 1, flexDirection: 'row', height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center', gap: 6 },
btnActionText: { fontSize: 14, fontWeight: '800' }
});

View File

@@ -195,7 +195,7 @@ const CriarAluno = () => {
const { error: alunoError } = await supabase
.from('alunos')
.insert([{
profile_id: user.id, // 🟢 CORREÇÃO AQUI: perfil_id igual ao do teu diagrama
profile_id: user.id,
nome,
n_escola: nEscola,
ano: ano ? parseInt(ano) : null,
@@ -214,7 +214,7 @@ const CriarAluno = () => {
tutor_nome: nome,
tutor_telefone: telefone,
curso: curso.toUpperCase()
// 🟢 CORREÇÃO AQUI: Removi user_id e setor que não existem no diagrama
}]);
if (empresaError) throw empresaError;
}
@@ -258,7 +258,7 @@ const CriarAluno = () => {
</TouchableOpacity>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.title, { color: cores.texto }]}>Novo Registo</Text>
<Text style={[styles.subtitle, { color: cores.laranja }]}>Sistema Central</Text>
<Text style={[styles.subtitle, { color: cores.laranja }]}>Estágios+</Text>
</View>
<View style={{ width: 45 }} />
</View>
@@ -307,7 +307,7 @@ const CriarAluno = () => {
{/* DADOS DA EMPRESA */}
{tipo === 'empresa' && (
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<SectionHeader icon="business" title="Dados da Entidade" cores={cores} />
<SectionHeader icon="business" title="Dados da Empresa" cores={cores} />
<View style={styles.inputGroup}>
<TextInput
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
@@ -316,7 +316,7 @@ const CriarAluno = () => {
<View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
value={curso} onChangeText={setCurso} placeholder="Curso Alvo" placeholderTextColor={cores.placeholder}
value={curso} onChangeText={setCurso} placeholder="Curso" placeholderTextColor={cores.placeholder}
/>
<TextInput
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
@@ -333,14 +333,14 @@ const CriarAluno = () => {
<View style={styles.inputGroup}>
<TextInput
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? 'Nome do Responsável/Tutor' : 'Nome Completo'} placeholderTextColor={cores.placeholder}
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? 'Nome' : 'Nome'} placeholderTextColor={cores.placeholder}
/>
{tipo !== 'empresa' && (
<View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput
style={[styles.input, { flex: 2, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
value={dataNascimento} onChangeText={(t) => setDataNascimento(aplicarMascaraData(t))} placeholder="Data de Nascimento (DD-MM-AAAA)" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder}
value={dataNascimento} onChangeText={(t) => setDataNascimento(aplicarMascaraData(t))} placeholder="Data de Nascimento" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder}
/>
<View style={[styles.input, { flex: 1, backgroundColor: cores.fundo, borderColor: cores.borda, justifyContent: 'center' }]}>
<Text style={{ color: idade ? cores.texto : cores.placeholder, textAlign: 'center', fontWeight: '700' }}>
@@ -369,7 +369,7 @@ const CriarAluno = () => {
<View style={{ flexDirection: 'row', gap: 10 }}>
<TextInput
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
value={ano} onChangeText={setAno} keyboardType="numeric" placeholder="Ano (Ex: 12)" placeholderTextColor={cores.placeholder}
value={ano} onChangeText={setAno} keyboardType="numeric" placeholder="Ano" placeholderTextColor={cores.placeholder}
/>
<TextInput
style={[styles.input, { flex: 2, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
@@ -378,7 +378,7 @@ const CriarAluno = () => {
</View>
<TextInput
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
value={curso} onChangeText={setCurso} placeholder="Turma/Curso (Ex: GPSI)" placeholderTextColor={cores.placeholder}
value={curso} onChangeText={setCurso} placeholder="Curso" placeholderTextColor={cores.placeholder}
/>
</View>
</View>

View File

@@ -127,6 +127,8 @@ export default function ProfessorHome() {
<MenuCard icon="calendar" title="Presenças" subtitle="Verifica a presença e a localização dos alunos" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} corDestaque={cores.azul} />
<MenuCard icon="document-text" title="Sumários" subtitle="Verifica os sumários dos alunos" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} corDestaque={cores.laranja} />
<MenuCard icon="alert-circle" title="Faltas" subtitle="Verifica as faltas e as justificações" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} corDestaque="#EF4444" />
<MenuCard icon="alert-circle" title="Relatórios" subtitle="Verifica e obtém relatórios" onPress={() => router.push('')} cores={cores} corDestaque={cores.azul} />
</View>
</View>