empresa atualizações

This commit is contained in:
2026-05-05 17:15:46 +01:00
parent da338b4ac4
commit 01f178a21e
8 changed files with 829 additions and 695 deletions

View File

@@ -387,12 +387,6 @@ const AlunoHome = memo(() => {
return { bg: themeStyles.verde + '20', text: themeStyles.verde, label: 'A DECORRER' };
};
const badgeObj = getBadgeStyle();
const horasTotais = estagioDetalhes?.horas_totais || 0;
const horasPorDia = Number(estagioDetalhes?.horas_diarias || 0);
const horasConcluidas = statsFaltas.totalPresencas * horasPorDia;
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
const renderAvisoEstadoDia = () => {
const reg = registosDiarios[selectedDate];
@@ -408,7 +402,6 @@ const badgeObj = getBadgeStyle();
if (!reg) {
// Se o dia não for válido para estágio ou for no futuro, não mostra nada
if (infoData.foraDeRange || !infoData.valida || selectedDate > hojeStr) return null;
// Se for um dia válido que já passou e está vazio, usa a config base (Azul)
} else if (reg.estado === 'presente') {
const temSumario = reg.sumario && reg.sumario.trim() !== '';
@@ -430,24 +423,18 @@ const badgeObj = getBadgeStyle();
}
return (
<View style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: config.bg,
padding: 16,
borderRadius: 16,
marginTop: 15,
borderWidth: 1,
borderColor: config.cor + '40' // Adiciona um bocado de transparência à borda
}}>
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: config.bg, padding: 16, borderRadius: 16, marginTop: 15, borderWidth: 1, borderColor: config.cor + '40' }}>
<Ionicons name={config.icon as any} size={28} color={config.cor} />
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: config.cor }}>
{config.texto}
</Text>
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: config.cor }}>{config.texto}</Text>
</View>
);
};
const horasTotais = Number(estagioDetalhes?.horas_totais) || 0;
const horasPorDia = Number(estagioDetalhes?.horas_diarias) || 0;
const horasConcluidas = (statsFaltas?.totalPresencas || 0) * horasPorDia;
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
const regSelecionado = registosDiarios[selectedDate];
return (
@@ -542,16 +529,16 @@ const badgeObj = getBadgeStyle();
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginTop: 4 }]} />
{activeTab === 'horas' && (
<View style={styles.dashGrid}>
<View>
{/* LINHA DE CIMA: CÁLCULO DAS HORAS */}
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>REALIZADAS</Text>
{/* 🟢 AGORA USA A VARIÁVEL CALCULADA */}
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{horasConcluidas}h</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>EM FALTA</Text>
{/* 🟢 AGORA USA A VARIÁVEL CALCULADA */}
<Text style={[styles.dashStatValue, { color: themeStyles.laranja }]}>{horasEmFalta}h</Text>
</View>
<View style={styles.dashDividerVertical} />
@@ -559,12 +546,15 @@ const badgeObj = getBadgeStyle();
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>TOTAIS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.texto }]}>{horasTotais}h</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
{/* LINHA DE BAIXO: ESTATÍSTICA DE PRESENÇAS E FALTAS */}
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>APROVADAS</Text>
{/* Corrigido para PRESENÇAS */}
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>PRESENÇAS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{statsFaltas.totalPresencas}</Text>
</View>
<View style={styles.dashDividerVertical} />

View File

@@ -1,482 +0,0 @@
// app/Empresa/EmpresaHome.tsx
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { useCallback, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
Animated,
Linking,
Modal,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../../lib/supabase';
import { useTheme } from '../../themecontext';
// Tipos de Ecrã possíveis neste "Tudo-em-Um"
type AppScreen = 'DASHBOARD' | 'ALUNOS' | 'PEDIDOS_LISTA' | 'PEDIDOS_HISTORICO' | 'AVALIACOES' | 'DEFINICOES';
export default function EmpresaHome() {
const { isDarkMode } = useTheme();
const router = useRouter();
// ESTADOS DE NAVEGAÇÃO E DADOS
const [currentScreen, setCurrentScreen] = useState<AppScreen>('DASHBOARD');
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [empresaData, setEmpresaData] = useState<any>(null);
const [listaAlunos, setListaAlunos] = useState<any[]>([]);
const [presencasGerais, setPresencasGerais] = useState<any[]>([]);
// ESTADOS PARA MODAIS E SELEÇÕES
const [alunoSelecionado, setAlunoSelecionado] = useState<any>(null);
const [modalDetalhesAluno, setModalDetalhesAluno] = useState(false);
// TOAST ANIMADO
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
const slideAnim = useRef(new Animated.Value(-100)).current;
const themeStyles = useMemo(() => ({
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
card: isDarkMode ? '#161618' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: '#2390a6',
laranja: '#E38E00',
verde: '#10B981',
vermelho: '#EF4444',
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
aviso: isDarkMode ? '#2D2200' : '#FFF9E6',
avisoTexto: isDarkMode ? '#FFD700' : '#856404'
}), [isDarkMode]);
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
setToast({ visible: true, message, type });
Animated.timing(slideAnim, { toValue: Platform.OS === 'ios' ? 50 : 20, duration: 300, useNativeDriver: true }).start(() => {
setTimeout(() => {
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
.start(() => setToast({ visible: false, message: '', type: 'info' }));
}, 3000);
});
}, [slideAnim]);
// 🚀 BUSCAR TUDO DE UMA VEZ
const fetchTudo = async (isManualRefresh = false) => {
if (!isManualRefresh) setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
// 1. Empresa
const { data: empresa } = await supabase.from('empresas').select('*').eq('user_id', user.id).maybeSingle();
if (!empresa) {
setLoading(false);
return Alert.alert("Erro", "Conta não associada a nenhuma empresa.");
}
setEmpresaData(empresa);
// 2. Alunos
const { data: estagios } = await supabase.from('estagios').select('aluno_id').eq('empresa_id', empresa.id);
const alunoIds = estagios?.map(e => e.aluno_id) || [];
if (alunoIds.length > 0) {
const { data: alunos } = await supabase.from('alunos').select('*').in('id', alunoIds);
setListaAlunos(alunos || []);
// 3. Presenças e Faltas
const { data: presencas } = await supabase
.from('presencas')
.select('*')
.in('aluno_id', alunoIds)
.order('data', { ascending: false });
setPresencasGerais(presencas || []);
} else {
setListaAlunos([]);
setPresencasGerais([]);
}
} catch (error) {
console.error(error);
showToast("Erro ao carregar dados", "error");
} finally {
if (!isManualRefresh) setLoading(false);
setRefreshing(false);
}
};
useFocusEffect(useCallback(() => { fetchTudo(); }, []));
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchTudo(true);
}, []);
// 🟢 APROVAR OU RECUSAR (COM VERIFICAÇÃO ANTI-MENTIRAS DO SUPABASE)
const lidarComPresenca = async (id: string, decisao: 'aprovado' | 'rejeitado') => {
try {
if (decisao === 'rejeitado') {
// Tenta apagar e pede confirmação de volta (.select)
const { data, error } = await supabase.from('presencas').delete().eq('id', id).select();
if (error) throw error;
if (!data || data.length === 0) throw new Error("A Base de Dados bloqueou a ação (Verifica se desligaste o RLS no Supabase)!");
showToast("Registo recusado e apagado!", "info");
// Remove da lista local da empresa
setPresencasGerais(prev => prev.filter(p => p.id !== id));
} else {
// Tenta atualizar e pede confirmação de volta (.select)
const { data, error } = await supabase.from('presencas').update({ estado_tutor: decisao }).eq('id', id).select();
if (error) throw error;
if (!data || data.length === 0) throw new Error("A Base de Dados bloqueou a ação (Verifica se desligaste o RLS no Supabase)!");
showToast("Validado com sucesso!", "success");
// Atualiza a lista local da empresa
setPresencasGerais(prev => prev.map(p => p.id === id ? { ...p, estado_tutor: decisao } : p));
}
} catch (e: any) {
Alert.alert("Erro a Validar", e.message || "Não foi possível alterar o registo na Base de Dados.");
}
};
const formatarData = (dataStr: string) => {
if (!dataStr) return '';
const parts = dataStr.split('-');
return parts.length !== 3 ? dataStr : `${parts[2]}/${parts[1]}/${parts[0]}`;
};
// ==========================================
// COMPONENTES DOS DIFERENTES ECRÃS (VIEWS)
// ==========================================
const renderDashboard = () => (
<View style={styles.grid}>
{/* CARD 1: ALUNOS */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('ALUNOS')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.azulSuave }]}>
<Ionicons name="people" size={32} color={themeStyles.azul} />
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Alunos</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Verificar estagiários e detalhes.</Text>
</TouchableOpacity>
{/* CARD 2: PEDIDOS */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('PEDIDOS_LISTA')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.laranja + '20' }]}>
<Ionicons name="documents" size={32} color={themeStyles.laranja} />
{presencasGerais.filter(p => p.estado_tutor === 'pendente').length > 0 && (
<View style={styles.badgeNotif} />
)}
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Pedidos</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Validar sumários e faltas.</Text>
</TouchableOpacity>
{/* CARD 3: AVALIAÇÕES */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('AVALIACOES')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.verde + '20' }]}>
<Ionicons name="star" size={32} color={themeStyles.verde} />
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Avaliações</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Em breve.</Text>
</TouchableOpacity>
{/* CARD 4: DEFINIÇÕES */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('DEFINICOES')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.secundario + '20' }]}>
<Ionicons name="settings" size={32} color={themeStyles.secundario} />
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Definições</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Gerir conta da empresa.</Text>
</TouchableOpacity>
</View>
);
const renderAlunos = () => (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar ao Menu</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Estagiários ({listaAlunos.length})</Text>
{listaAlunos.length === 0 ? (
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 40 }}>Nenhum aluno associado a esta empresa.</Text>
) : (
listaAlunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
style={[styles.listCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => { setAlunoSelecionado(aluno); setModalDetalhesAluno(true); }}
>
<View style={[styles.avatar, { backgroundColor: themeStyles.azulSuave }]}>
<Text style={[styles.avatarText, { color: themeStyles.azul }]}>{aluno.nome.charAt(0)}</Text>
</View>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.listCardTitle, { color: themeStyles.texto }]}>{aluno.nome}</Text>
<Text style={[styles.listCardSub, { color: themeStyles.secundario }]}>Toque para ver detalhes</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={themeStyles.secundario} />
</TouchableOpacity>
))
)}
</View>
);
const renderPedidosLista = () => (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar ao Menu</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Validar Registos</Text>
<Text style={{ color: themeStyles.secundario, marginBottom: 20 }}>Selecione um aluno para ver o histórico e validar pedidos pendentes.</Text>
{listaAlunos.map(aluno => {
const pendentesDoAluno = presencasGerais.filter(p => p.aluno_id === aluno.id && p.estado_tutor === 'pendente').length;
return (
<TouchableOpacity
key={aluno.id}
style={[styles.listCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => { setAlunoSelecionado(aluno); setCurrentScreen('PEDIDOS_HISTORICO'); }}
>
<View style={{ flex: 1 }}>
<Text style={[styles.listCardTitle, { color: themeStyles.texto }]}>{aluno.nome}</Text>
</View>
{pendentesDoAluno > 0 ? (
<View style={[styles.badgeCount, { backgroundColor: themeStyles.vermelho }]}>
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 12 }}>{pendentesDoAluno} PENDENTES</Text>
</View>
) : (
<Ionicons name="checkmark-circle" size={24} color={themeStyles.verde} />
)}
</TouchableOpacity>
);
})}
</View>
);
const renderPedidosHistorico = () => {
if (!alunoSelecionado) return null;
const historicoAluno = presencasGerais.filter(p => p.aluno_id === alunoSelecionado.id);
return (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('PEDIDOS_LISTA')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar à Lista</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto, fontSize: 20 }]}>Histórico: {alunoSelecionado.nome}</Text>
{historicoAluno.length === 0 ? (
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 40 }}>Nenhum registo efetuado por este aluno.</Text>
) : (
historicoAluno.map(item => (
<View key={item.id} style={[styles.historyCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.historyTop}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>{formatarData(item.data)}</Text>
<View style={[styles.statusTag, { backgroundColor: item.estado === 'faltou' ? themeStyles.vermelho + '15' : themeStyles.verde + '15' }]}>
<Text style={[styles.statusTagText, { color: item.estado === 'faltou' ? themeStyles.vermelho : themeStyles.verde }]}>
{item.estado === 'faltou' ? 'FALTA' : 'PRESENÇA'}
</Text>
</View>
</View>
<View style={[styles.historyBody, { backgroundColor: themeStyles.fundo }]}>
<Text style={{ color: themeStyles.texto, fontWeight: '600' }}>
{item.estado === 'presente' ? (item.sumario || "Sem sumário registado.") : "Aluno marcou ausência."}
</Text>
{item.justificacao_url && (
<TouchableOpacity style={{ marginTop: 10, flexDirection: 'row', alignItems: 'center' }} onPress={() => Linking.openURL(item.justificacao_url)}>
<Ionicons name="document-attach" size={16} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', marginLeft: 5 }}>Ver Justificativo PDF</Text>
</TouchableOpacity>
)}
</View>
{/* SÓ MOSTRA BOTÕES SE ESTIVER PENDENTE */}
{item.estado_tutor === 'pendente' ? (
<View style={styles.actionRow}>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]} onPress={() => lidarComPresenca(item.id, 'rejeitado')}>
<Ionicons name="trash-outline" size={20} color={themeStyles.vermelho} />
<Text style={{ color: themeStyles.vermelho, fontWeight: '800', marginLeft: 5 }}>Recusar (Apagar)</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: themeStyles.verde }]} onPress={() => lidarComPresenca(item.id, 'aprovado')}>
<Ionicons name="checkmark" size={20} color="#fff" />
<Text style={{ color: '#fff', fontWeight: '800', marginLeft: 5 }}>Aprovar</Text>
</TouchableOpacity>
</View>
) : (
<Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '800', color: themeStyles.verde }}>
Aprovado
</Text>
)}
</View>
))
)}
</View>
);
};
const renderAvaliacoes = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<TouchableOpacity style={[styles.btnVoltar, { position: 'absolute', top: 0, left: 0 }]} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar</Text>
</TouchableOpacity>
<Ionicons name="construct" size={80} color={themeStyles.secundario} style={{ opacity: 0.3 }} />
<Text style={{ fontSize: 24, fontWeight: '900', color: themeStyles.texto, marginTop: 20 }}>BREVEMENTE</Text>
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 10 }}>A área de avaliações finais está em construção.</Text>
</View>
);
const renderDefinicoes = () => (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Definições</Text>
<View style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, width: '100%', marginBottom: 20 }]}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800', fontSize: 12, textTransform: 'uppercase' }}>Nome da Empresa</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18, marginTop: 5 }}>{empresaData?.nome || 'N/A'}</Text>
<Text style={{ color: themeStyles.secundario, fontWeight: '800', fontSize: 12, textTransform: 'uppercase', marginTop: 15 }}>Tutor</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18, marginTop: 5 }}>{empresaData?.tutor_nome || 'N/A'}</Text>
</View>
<TouchableOpacity
style={{ backgroundColor: themeStyles.vermelho, padding: 18, borderRadius: 16, alignItems: 'center', flexDirection: 'row', justifyContent: 'center' }}
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
>
<Ionicons name="log-out" size={24} color="#fff" />
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 16, marginLeft: 10 }}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
);
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<Animated.View style={[
styles.toastContainer,
{ transform: [{ translateY: slideAnim }] },
toast.type === 'error' ? { backgroundColor: themeStyles.vermelho } :
toast.type === 'success' ? { backgroundColor: themeStyles.verde } :
{ backgroundColor: themeStyles.azul }
]}>
<Ionicons name={toast.type === 'error' ? "warning" : "checkmark-circle"} size={22} color="#FFF" />
<Text style={styles.toastText}>{toast.message}</Text>
</Animated.View>
<View style={styles.headerArea}>
<Text style={[styles.appTitle, { color: themeStyles.texto }]}>Painel Empresa</Text>
{empresaData?.nome ? <Text style={{ color: themeStyles.secundario, fontWeight: '700' }}>{empresaData.nome}</Text> : null}
</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} tintColor={themeStyles.azul} />}
>
{currentScreen === 'DASHBOARD' && renderDashboard()}
{currentScreen === 'ALUNOS' && renderAlunos()}
{currentScreen === 'PEDIDOS_LISTA' && renderPedidosLista()}
{currentScreen === 'PEDIDOS_HISTORICO' && renderPedidosHistorico()}
{currentScreen === 'AVALIACOES' && renderAvaliacoes()}
{currentScreen === 'DEFINICOES' && renderDefinicoes()}
</ScrollView>
)}
{/* MODAL DETALHES DO ALUNO */}
<Modal visible={modalDetalhesAluno} animationType="slide" transparent>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: themeStyles.card }]}>
<View style={styles.modalHandle} />
<Text style={[styles.pageTitle, { color: themeStyles.texto, textAlign: 'center' }]}>{alunoSelecionado?.nome}</Text>
<View style={[styles.infoBox, { backgroundColor: themeStyles.fundo }]}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}> ESCOLA</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18 }}>{alunoSelecionado?.n_escola || 'N/A'}</Text>
</View>
<View style={[styles.infoBox, { backgroundColor: themeStyles.fundo, marginTop: 10 }]}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>TURMA/CURSO</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18 }}>{alunoSelecionado?.turma_curso || 'N/A'}</Text>
</View>
<TouchableOpacity style={[styles.btnFecharModal, { backgroundColor: themeStyles.azul }]} onPress={() => setModalDetalhesAluno(false)}>
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 16 }}>Fechar</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6 },
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12 },
headerArea: { paddingHorizontal: 20, paddingTop: 20, paddingBottom: 10 },
appTitle: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 },
scroll: { padding: 20, paddingBottom: 60 },
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
// DASHBOARD GRID
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
dashCard: { width: '48%', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
dashIcon: { width: 56, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
dashTitle: { fontSize: 18, fontWeight: '900', marginBottom: 5 },
dashDesc: { fontSize: 12, fontWeight: '600', lineHeight: 18 },
badgeNotif: { position: 'absolute', top: -5, right: -5, width: 14, height: 14, borderRadius: 7, backgroundColor: '#EF4444', borderWidth: 2, borderColor: '#fff' },
// SUB-PAGES
btnVoltar: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
pageTitle: { fontSize: 24, fontWeight: '900', marginBottom: 20 },
// LISTAS
listCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, borderWidth: 1, marginBottom: 12 },
avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
avatarText: { fontSize: 18, fontWeight: '900' },
listCardTitle: { fontSize: 16, fontWeight: '800' },
listCardSub: { fontSize: 12, fontWeight: '600', marginTop: 2 },
badgeCount: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12 },
// HISTÓRICO
historyCard: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
historyTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
statusTag: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
statusTagText: { fontSize: 10, fontWeight: '900', letterSpacing: 0.5 },
historyBody: { padding: 15, borderRadius: 16, marginBottom: 15 },
actionRow: { flexDirection: 'row', gap: 12 },
btnAction: { flex: 1, flexDirection: 'row', height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
// MODAL
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 30, alignItems: 'center', paddingBottom: 50 },
modalHandle: { width: 50, height: 6, backgroundColor: '#cbd5e1', borderRadius: 10, marginBottom: 20 },
infoBox: { width: '100%', padding: 15, borderRadius: 16, alignItems: 'center' },
btnFecharModal: { width: '100%', padding: 18, borderRadius: 16, alignItems: 'center', marginTop: 20 }
});

View File

@@ -1,12 +1,11 @@
// app/Empresas/EmpresaHome.tsx
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,
@@ -22,9 +21,7 @@ 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(() => ({
@@ -37,238 +34,150 @@ export default function EmpresaHome() {
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);
const fetchEmpresaInfo = async () => {
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')
.select('nome')
.eq('user_id', user.id)
.single();
if (!empresa) {
setPendentes([]);
return;
if (empresa) {
setEmpresaNome(empresa.nome);
}
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);
setLoading(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]}`;
};
useFocusEffect(useCallback(() => { fetchEmpresaInfo(); }, []));
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* CABEÇALHO */}
<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>
{loading ? (
<ActivityIndicator size="small" color={themeStyles.azul} style={{ marginTop: 5, alignSelf: 'flex-start' }} />
) : (
<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} />}
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* CARTÃO DE DESTAQUE - PEDIDOS PENDENTES */}
<Text style={[styles.sectionLabel, { color: themeStyles.textoSecundario }]}>AÇÃO IMEDIATA</Text>
<TouchableOpacity
activeOpacity={0.8}
style={[styles.heroCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: themeStyles.laranja }]}
onPress={() => router.push('/Empresas/pedidos')}
>
{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 style={styles.heroContent}>
<View style={[styles.iconWrapperLarge, { backgroundColor: themeStyles.laranja + '15' }]}>
<Ionicons name="document-text" size={34} color={themeStyles.laranja} />
</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.heroTextContainer}>
<Text style={[styles.heroTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
<Text style={[styles.heroDesc, { color: themeStyles.textoSecundario }]}>Aprovar presenças e sumários</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={24} color={themeStyles.textoSecundario} />
</TouchableOpacity>
<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>
{/* SECÇÃO GESTÃO - GRELHA 2 COLUNAS */}
<Text style={[styles.sectionLabel, { color: themeStyles.textoSecundario, marginTop: 15 }]}>GESTÃO DE ESTÁGIOS</Text>
<View style={styles.gridContainer}>
<TouchableOpacity
activeOpacity={0.7}
style={[styles.gridCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => router.push('/Empresas/alunos')}
>
<View style={[styles.iconWrapper, { backgroundColor: themeStyles.azul + '15' }]}>
<Ionicons name="people" size={26} color={themeStyles.azul} />
</View>
<Text style={[styles.gridCardTitle, { color: themeStyles.texto }]}>Alunos</Text>
<Text style={[styles.gridCardDesc, { color: themeStyles.textoSecundario }]}>Gerir estagiários</Text>
</TouchableOpacity>
<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
activeOpacity={0.7}
style={[styles.gridCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => router.push('/Empresas/avaliacoesEmpresa')}
>
<View style={[styles.iconWrapper, { backgroundColor: themeStyles.verde + '15' }]}>
<Ionicons name="star" size={26} color={themeStyles.verde} />
</View>
<Text style={[styles.gridCardTitle, { color: themeStyles.texto }]}>Avaliações</Text>
<Text style={[styles.gridCardDesc, { color: themeStyles.textoSecundario }]}>Avaliar estágios</Text>
</TouchableOpacity>
</View>
<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>
{/* DEFINIÇÕES - CARTÃO LARGO INFERIOR */}
<TouchableOpacity
activeOpacity={0.7}
style={[styles.rowCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => router.push('/Empresas/definicoesEmpresa')}
>
<View style={[styles.iconWrapperSmall, { backgroundColor: themeStyles.textoSecundario + '15' }]}>
<Ionicons name="settings" size={22} color={themeStyles.textoSecundario} />
</View>
<View style={styles.rowTextContainer}>
<Text style={[styles.rowTitle, { color: themeStyles.texto }]}>Definições da Conta</Text>
<Text style={[styles.rowDesc, { color: themeStyles.textoSecundario }]}>Ajustar dados da empresa e segurança</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={themeStyles.textoSecundario} />
</TouchableOpacity>
</View>
))
)}
</ScrollView>
)}
</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 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 15 },
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' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
sectionLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 1.2, marginBottom: 12, marginLeft: 5 },
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 },
heroCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, borderRadius: 24, borderWidth: 1, borderLeftWidth: 6, marginBottom: 25, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.05, shadowRadius: 10 },
heroContent: { flexDirection: 'row', alignItems: 'center', flex: 1 },
iconWrapperLarge: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
heroTextContainer: { flex: 1, paddingRight: 10 },
heroTitle: { fontSize: 19, fontWeight: '900', marginBottom: 4 },
heroDesc: { fontSize: 13, fontWeight: '600' },
sumarioBox: { padding: 15, borderRadius: 16, marginBottom: 15 },
sumarioLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6 },
sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 20 },
gridContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
gridCard: { width: '48%', padding: 20, borderRadius: 24, borderWidth: 1, alignItems: 'flex-start', elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 },
iconWrapper: { padding: 12, borderRadius: 16, marginBottom: 16 },
gridCardTitle: { fontSize: 16, fontWeight: '900', marginBottom: 4 },
gridCardDesc: { fontSize: 12, fontWeight: '600' },
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' }
rowCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, borderWidth: 1, marginTop: 5 },
iconWrapperSmall: { width: 45, height: 45, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
rowTextContainer: { flex: 1 },
rowTitle: { fontSize: 15, fontWeight: '800', marginBottom: 2 },
rowDesc: { fontSize: 12, fontWeight: '600' }
});

241
app/Empresas/alunos.tsx Normal file
View File

@@ -0,0 +1,241 @@
// app/Empresa/alunos.tsx
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 GestaoAlunos() {
const { isDarkMode } = useTheme();
const router = useRouter();
const [alunosList, setAlunosList] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
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',
verde: '#10B981',
laranja: '#dd8707',
}), [isDarkMode]);
const fetchAlunos = async (isManualRefresh = false) => {
if (!isManualRefresh) setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: empresa } = await supabase
.from('empresas')
.select('id')
.eq('user_id', user.id)
.single();
if (!empresa) {
setAlunosList([]);
return;
}
// 🐅 GRAÇAS AO TIGRE, BASTA LER A COLUNA horas_concluidas DIRETO DA BASE DE DADOS!
const { data: estagios, error } = await supabase
.from('estagios')
.select(`
id,
data_inicio,
data_fim,
horas_totais,
horas_concluidas,
alunos (id, nome)
`)
.eq('empresa_id', empresa.id)
.order('data_fim', { ascending: false });
if (error) throw error;
const formatados = estagios?.map((estagio: any) => {
// 🟢 TRUQUE ANTI-ERRO: Tira o aluno da lista se o Supabase mandar em formato Array
const alunoObj = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos;
return {
id_estagio: estagio.id,
aluno_id: alunoObj?.id,
aluno_nome: alunoObj?.nome || 'Aluno Desconhecido',
data_inicio: estagio.data_inicio,
data_fim: estagio.data_fim,
horas_totais: estagio.horas_totais || 0,
horas_concluidas: estagio.horas_concluidas || 0, // <-- LIDO DIRETO DA TABELA!
};
}) || [];
setAlunosList(formatados);
} catch (error) {
console.error(error);
Alert.alert("Erro", "Não foi possível carregar a lista de alunos.");
} finally {
if (!isManualRefresh) setLoading(false);
setRefreshing(false);
}
};
useFocusEffect(useCallback(() => { fetchAlunos(); }, []));
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchAlunos(true);
}, []);
const formatarData = (dataStr: string) => {
if (!dataStr) return '';
const parts = dataStr.split('-');
if (parts.length !== 3) return dataStr;
return `${parts[2]}/${parts[1]}/${parts[0]}`;
};
const getStatus = (dataFim: string) => {
const hoje = new Date().toISOString().split('T')[0];
if (hoje > dataFim) return { texto: 'CONCLUÍDO', cor: themeStyles.textoSecundario, bg: themeStyles.textoSecundario + '20' };
return { texto: 'A DECORRER', cor: themeStyles.verde, bg: themeStyles.verde + '20' };
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: themeStyles.texto }]}>Os Meus Alunos</Text>
<View style={{ width: 24 }} />
</View>
<View style={styles.headerTitleContainer}>
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Estagiários Alocados</Text>
<View style={[styles.badge, { backgroundColor: themeStyles.azul + '20' }]}>
<Text style={[styles.badgeText, { color: themeStyles.azul }]}>{alunosList.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} />}
>
{alunosList.length === 0 ? (
<View style={[styles.emptyBox, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}>
<Ionicons name="people-circle" size={60} color={themeStyles.azul} style={{ marginBottom: 15 }} />
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Nenhum estagiário!</Text>
<Text style={[styles.emptyDesc, { color: themeStyles.textoSecundario }]}>
A tua empresa ainda não tem alunos associados pelo professor neste momento.
</Text>
</View>
) : (
alunosList.map((aluno, index) => {
const status = getStatus(aluno.data_fim);
const progressoPercent = aluno.horas_totais > 0 ? (aluno.horas_concluidas / aluno.horas_totais) * 100 : 0;
return (
<View
key={index}
style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: themeStyles.azul }]}
>
<View style={styles.cardHeader}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: 10 }}>
<View style={[styles.avatar, { backgroundColor: themeStyles.azul + '20' }]}>
<Ionicons name="person" size={18} color={themeStyles.azul} />
</View>
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>
{aluno.aluno_nome}
</Text>
</View>
<View style={[styles.statusTag, { backgroundColor: status.bg }]}>
<Text style={[styles.statusTagText, { color: status.cor }]}>{status.texto}</Text>
</View>
</View>
<View style={[styles.divider, { backgroundColor: themeStyles.borda }]} />
<View style={styles.infoRow}>
<View style={styles.infoCol}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>INÍCIO</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{formatarData(aluno.data_inicio)}</Text>
</View>
<View style={styles.infoCol}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>FIM PREVISTO</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{formatarData(aluno.data_fim)}</Text>
</View>
</View>
<View style={styles.progressSection}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>HORAS REALIZADAS</Text>
<Text style={[styles.horasValue, { color: themeStyles.azul }]}>{aluno.horas_concluidas} / {aluno.horas_totais}h</Text>
</View>
<View style={[styles.progressBarBg, { backgroundColor: themeStyles.borda }]}>
<View style={[styles.progressBarFill, { backgroundColor: themeStyles.azul, width: `${Math.min(progressoPercent, 100)}%` }]} />
</View>
</View>
</View>
);
})
)}
</ScrollView>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 15 },
btnVoltar: { padding: 5, marginLeft: -5 },
headerTitle: { fontSize: 20, fontWeight: '900' },
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
headerTitleContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 20, gap: 10 },
sectionTitle: { fontSize: 16, 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: 20, borderWidth: 1, borderLeftWidth: 5, marginBottom: 15, elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
avatar: { width: 36, height: 36, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
alunoName: { fontSize: 17, fontWeight: '900', flex: 1 },
statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
divider: { height: 1, marginVertical: 15, opacity: 0.5 },
infoRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 },
infoCol: { flex: 1 },
infoLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 4 },
infoValue: { fontSize: 14, fontWeight: '700' },
progressSection: { marginTop: 5 },
horasValue: { fontSize: 12, fontWeight: '800' },
progressBarBg: { height: 8, borderRadius: 4, width: '100%', overflow: 'hidden' },
progressBarFill: { height: '100%', borderRadius: 4 }
});

View File

View File

@@ -0,0 +1,249 @@
// app/Definicoes.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useCallback, useMemo, useRef, useState } from 'react';
import {
Animated,
Linking,
ScrollView,
StatusBar,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { supabase } from '../../lib/supabase';
import { useTheme } from '../../themecontext';
const Definicoes = memo(() => {
const router = useRouter();
const insets = useSafeAreaInsets();
const { isDarkMode, toggleTheme } = useTheme();
const [notificacoes, setNotificacoes] = useState(true);
// TOAST ANIMADO UNIVERSAL
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
const slideAnim = useRef(new Animated.Value(-100)).current;
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
setToast({ visible: true, message, type });
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
setTimeout(() => {
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
.start(() => setToast({ visible: false, message: '', type: 'info' }));
}, 3500);
});
}, [insets.top, slideAnim]);
// Cores EPVC
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const erroCor = '#EF4444';
const sucessoCor = '#10B981';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design Premium
card: isDarkMode ? '#161618' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
vermelho: erroCor,
verde: sucessoCor,
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
}), [isDarkMode]);
const handleLogout = async () => {
try {
await supabase.auth.signOut();
router.replace('/');
} catch (e) {
showToast("Erro ao sair da conta", "error");
}
};
const abrirURL = (url: string) => {
Linking.canOpenURL(url).then(supported => {
if (supported) Linking.openURL(url);
else showToast("Não foi possível abrir o link", "error");
});
};
return (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
{/* 🟢 TOAST ANIMADO NO TOPO */}
<Animated.View style={[
styles.toastContainer,
{ transform: [{ translateY: slideAnim }] },
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
toast.type === 'success' ? { backgroundColor: cores.verde } :
{ backgroundColor: cores.azul }
]}>
<Ionicons
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
size={24}
color="#FFF"
/>
<Text style={styles.toastText}>{toast.message}</Text>
</Animated.View>
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* HEADER MODERNO */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { borderColor: cores.borda, backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={{alignItems: 'center'}}>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<Text style={[styles.tituloSub, { color: cores.laranja }]}>Sistema & Suporte</Text>
</View>
<View style={{ width: 44 }} />
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Preferências</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.item, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="notifications-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações</Text>
<Switch
value={notificacoes}
onValueChange={(v) => {
setNotificacoes(v);
showToast(v ? "Notificações ligadas" : "Notificações desligadas", "info");
}}
trackColor={{ false: cores.borda, true: cores.laranja }}
thumbColor="#FFFFFF"
/>
</View>
<View style={styles.item}>
<View style={[styles.iconContainer, { backgroundColor: isDarkMode ? '#334155' : '#F1F5F9' }]}>
<Ionicons name={isDarkMode ? "moon-outline" : "sunny-outline"} size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Modo Escuro</Text>
<Switch
value={isDarkMode}
onValueChange={toggleTheme}
trackColor={{ false: cores.borda, true: cores.laranja }}
thumbColor="#FFFFFF"
/>
</View>
</View>
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 30 }]}>Escola Profissional</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<SettingLink
icon="at-outline"
label="Direção Geral"
subLabel="epvc@epvc.pt"
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
cores={cores}
showBorder
/>
<SettingLink
icon="mail-unread-outline"
label="Secretaria"
subLabel="secretaria@epvc.pt"
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
cores={cores}
showBorder
/>
<SettingLink
icon="call-outline"
label="Linha Direta"
subLabel="252 641 805"
onPress={() => abrirURL('tel:252641805')}
cores={cores}
/>
</View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 30 }]}>
<View style={[styles.item, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="code-working-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>App Version</Text>
<Text style={[styles.versionBadge, { color: cores.azul, backgroundColor: cores.azulSuave }]}>2.6.17</Text>
</View>
<TouchableOpacity style={styles.item} onPress={handleLogout} activeOpacity={0.7}>
<View style={[styles.iconContainer, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '900' }]}>Terminar Sessão</Text>
<Ionicons name="arrow-forward-circle-outline" size={20} color={cores.vermelho} opacity={0.5} />
</TouchableOpacity>
</View>
<View style={styles.footer}>
<View style={[styles.footerLine, { backgroundColor: cores.laranja }]} />
<Text style={[styles.footerText, { color: cores.secundario }]}>Estágios+ Vila do Conde</Text>
<Text style={[styles.footerText, { color: cores.azul, marginTop: 4, fontSize: 13 }]}>EPVC</Text>
</View>
</ScrollView>
</SafeAreaView>
</View>
);
});
const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => (
<TouchableOpacity
style={[styles.item, showBorder && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
onPress={onPress}
activeOpacity={0.7}
>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>{label}</Text>
<Text style={{ color: cores.secundario, fontSize: 12, fontWeight: '600' }}>{subLabel}</Text>
</View>
<View style={[styles.arrowCircle, { borderColor: cores.borda, backgroundColor: cores.fundo }]}>
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
</View>
</TouchableOpacity>
);
const styles = StyleSheet.create({
safe: { flex: 1 },
// TOAST STYLES
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
btnVoltar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
tituloGeral: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
tituloSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
scrollContent: { paddingHorizontal: 20, paddingTop: 10, paddingBottom: 60 },
sectionLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 0.8 },
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 },
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 18 },
iconContainer: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
versionBadge: { fontSize: 12, fontWeight: '900', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 },
arrowCircle: { width: 34, height: 34, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
footer: { alignItems: 'center', marginTop: 50 },
footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 },
footerText: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }
});
export default Definicoes;

227
app/Empresas/pedidos.tsx Normal file
View File

@@ -0,0 +1,227 @@
// app/Empresa/pedidos.tsx
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 PedidosPendentes() {
const { isDarkMode } = useTheme();
const router = useRouter();
const [pendentes, setPendentes] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
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',
}), [isDarkMode]);
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
if (!isManualRefresh) setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: empresa } = await supabase
.from('empresas')
.select('id')
.eq('user_id', user.id)
.single();
if (!empresa) {
setPendentes([]);
return;
}
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);
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; });
// Busca presenças OU faltas que estejam pendentes de aprovação da empresa
const { data: presencas } = await supabase
.from('presencas')
.select('*')
.in('aluno_id', alunoIds)
.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 os pedidos.");
} finally {
if (!isManualRefresh) setLoading(false);
setRefreshing(false);
}
};
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchValidaçõesPendentes(true);
}, []);
const formatarData = (dataStr: string) => {
if (!dataStr) return '';
const parts = dataStr.split('-');
if (parts.length !== 3) return dataStr;
return `${parts[2]}/${parts[1]}/${parts[0]}`;
};
const getTipoInfo = (item: any) => {
if (item.estado === 'presente') return { icone: 'checkmark-circle', cor: themeStyles.verde, texto: 'Presença e Sumário' };
if (item.justificacao_url) return { icone: 'document-text', cor: themeStyles.laranja, texto: 'Falta com Justificação' };
return { icone: 'close-circle', cor: themeStyles.laranja, texto: 'Falta a Confirmar' };
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: themeStyles.texto }]}>Pedidos</Text>
<View style={{ width: 24 }} />
</View>
<View style={styles.headerTitleContainer}>
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>A Aguardar Validação</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 limpo!</Text>
<Text style={[styles.emptyDesc, { color: themeStyles.textoSecundario }]}>
Não tens pedidos pendentes neste momento.
</Text>
</View>
) : (
pendentes.map((item, index) => {
const tipo = getTipoInfo(item);
return (
<TouchableOpacity
key={index}
activeOpacity={0.7}
style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: tipo.cor }]}
onPress={() => {
router.push({
pathname: '/Empresa/validar-registo',
params: {
aluno_id: item.aluno_id,
aluno_nome: item.aluno_nome,
data: item.data,
sumario: item.sumario || '',
estado: item.estado,
justificacao_url: item.justificacao_url || ''
}
});
}}
>
<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>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 4 }}>
<Text style={[styles.dataText, { color: themeStyles.textoSecundario }]}>
<Ionicons name="calendar-outline" size={12} /> {formatarData(item.data)}
</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
<Ionicons name={tipo.icone as any} size={14} color={tipo.cor} />
<Text style={{ fontSize: 12, fontWeight: '700', color: tipo.cor }}>{tipo.texto}</Text>
</View>
</View>
</View>
<Ionicons name="chevron-forward" size={24} color={themeStyles.textoSecundario} />
</View>
</TouchableOpacity>
);
})
)}
</ScrollView>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 15 },
btnVoltar: { padding: 5, marginLeft: -5 },
headerTitle: { fontSize: 20, fontWeight: '900' },
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
headerTitleContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 20, gap: 10 },
sectionTitle: { fontSize: 16, 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: 20, borderWidth: 1, borderLeftWidth: 5, marginBottom: 15, elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
alunoName: { fontSize: 17, fontWeight: '900', marginBottom: 2 },
dataText: { fontSize: 13, fontWeight: '700' }
});

View File

@@ -51,7 +51,7 @@ export default function LoginScreen() {
} else if (data.tipo === 'aluno') {
router.replace('/Aluno/AlunoHome');
} else if (data.tipo === 'empresa') {
router.replace('/Empresa/EmpresaHome'); // 🟢 Rota da empresa adicionada!
router.replace('/Empresas/EmpresaHome');
} else {
Alert.alert('Erro', 'Tipo de conta inválido');
}