hjjhjhgj
This commit is contained in:
274
app/Empresas/EmpresaHome.tsx
Normal file
274
app/Empresas/EmpresaHome.tsx
Normal 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' }
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user