diff --git a/app/Empresa/EmpresaHome.tsx b/app/Empresa/EmpresaHome.tsx new file mode 100644 index 0000000..7284aa5 --- /dev/null +++ b/app/Empresa/EmpresaHome.tsx @@ -0,0 +1,307 @@ +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, + Animated, + 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([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [empresaNome, setEmpresaNome] = useState(''); + + // Estados do 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 azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + + const themeStyles = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + azul: azulEPVC, + laranja: laranjaEPVC, + verde: '#10B981', + vermelho: '#EF4444', + azulSuave: '#00c3ff', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + }), [isDarkMode]); + + const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => { + setToast({ visible: true, message, type }); + Animated.timing(slideAnim, { toValue: 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]); + + const fetchValidaçõesPendentes = async (isManualRefresh = false) => { + if (!isManualRefresh) setLoading(true); + try { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return; + + // 1. Identificar a empresa logada + const { data: empresa, error: empError } = await supabase + .from('empresas') + .select('id, nome') + .eq('user_id', user.id) + .maybeSingle(); + + if (empError || !empresa) { + setPendentes([]); + return; + } + setEmpresaNome(empresa.nome); + + // 2. Buscar alunos vinculados (estágios) + 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 nomes dos alunos + const { data: alunos } = await supabase + .from('alunos') + .select('id, nome') + .in('id', alunoIds); + + const mapaAlunos: Record = {}; + alunos?.forEach(a => { mapaAlunos[a.id] = a.nome; }); + + // 4. Buscar apenas PRESENÇAS pendentes de validação + // IMPORTANTE: Faltas não aparecem aqui, vão direto para o professor. + 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); + showToast("Falha ao carregar validações.", "error"); + } finally { + if (!isManualRefresh) setLoading(false); + setRefreshing(false); + } + }; + + useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, [])); + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchValidaçõesPendentes(true); + }, []); + + const lidarComPresenca = async (id: string, decisao: 'aprovado' | 'rejeitado') => { + try { + const { error } = await supabase + .from('presencas') + .update({ estado_tutor: decisao }) + .eq('id', id); + + if (error) throw error; + + showToast(decisao === 'aprovado' ? "Registo aprovado!" : "Registo rejeitado.", decisao === 'aprovado' ? 'success' : 'info'); + setPendentes(prev => prev.filter(p => p.id !== id)); + + } catch (e: any) { + showToast("Erro ao processar validação.", "error"); + } + }; + + const formatarData = (dataStr: string) => { + if (!dataStr) return ''; + const parts = dataStr.split('-'); + return parts.length !== 3 ? dataStr : `${parts[2]}/${parts[1]}/${parts[0]}`; + }; + + return ( + + + + {/* TOAST ANIMADO */} + + + {toast.message} + + + + + Bem-vindo, + + {empresaNome || 'Entidade'} + + + supabase.auth.signOut().then(() => router.replace('/'))} + > + + + + + + Validações Pendentes + + {pendentes.length} + + + + {loading && !refreshing ? ( + + + + ) : ( + } + > + {pendentes.length === 0 ? ( + + + + + Tudo em ordem! + + Não existem registos pendentes de validação. **Vai dar merda** se os alunos não trabalharem! 😂 + + + ) : ( + pendentes.map((item) => ( + + + + {item.aluno_nome.charAt(0)} + + + {item.aluno_nome} + + + {formatarData(item.data)} + + + + PENDENTE + + + + + SUMÁRIO DO DIA: + + {item.sumario || "O aluno não descreveu as atividades deste dia."} + + + + + lidarComPresenca(item.id, 'rejeitado')} + > + + Rejeitar + + + lidarComPresenca(item.id, 'aprovado')} + > + + Aprovar + + + + )) + )} + + )} + + ); +} + +const styles = StyleSheet.create({ + safeArea: { flex: 1 }, + 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 }, + + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 20 }, + greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, + title: { fontSize: 24, fontWeight: '900', marginTop: 2 }, + logoutBtn: { width: 48, height: 48, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + + sectionHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 15, gap: 10 }, + sectionTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, + countBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10 }, + countText: { color: '#fff', fontSize: 12, fontWeight: '900' }, + + scroll: { paddingHorizontal: 20, paddingBottom: 40 }, + centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + + emptyBox: { alignItems: 'center', padding: 40, borderRadius: 28, borderWidth: 1, borderStyle: 'dashed', marginTop: 20 }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + emptyTitle: { fontSize: 20, fontWeight: '900', marginBottom: 8 }, + emptyDesc: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '600', opacity: 0.8 }, + + card: { padding: 20, borderRadius: 28, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 10 }, + cardTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 }, + avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 18, fontWeight: '900' }, + alunoName: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + dataRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 }, + dataText: { fontSize: 13, fontWeight: '700' }, + statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8 }, + statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 }, + + sumarioBox: { padding: 16, borderRadius: 18, marginBottom: 18 }, + sumarioLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, letterSpacing: 0.5 }, + sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 22 }, + + actionRow: { flexDirection: 'row', gap: 12 }, + btnAction: { flex: 1, flexDirection: 'row', height: 52, borderRadius: 16, justifyContent: 'center', alignItems: 'center', gap: 8 }, + btnActionText: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 } +}); \ No newline at end of file diff --git a/app/Empresas/EmpresaHome.tsx b/app/Empresas/EmpresaHome.tsx deleted file mode 100644 index 7ba0174..0000000 --- a/app/Empresas/EmpresaHome.tsx +++ /dev/null @@ -1,274 +0,0 @@ -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([]); - 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 = {}; - 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 ( - - - - - - Painel da Entidade - {empresaNome || 'A carregar...'} - - supabase.auth.signOut().then(() => router.replace('/'))}> - - - - - - Validações Pendentes - - {pendentes.length} - - - - {loading && !refreshing ? ( - - - - ) : ( - } - > - {pendentes.length === 0 ? ( - - - Tudo em dia! - - Não tens sumários ou presenças de alunos a aguardar a tua validação neste momento. - - - ) : ( - pendentes.map((item, index) => ( - - - - - - {item.aluno_nome} - - - {formatarData(item.data)} - - - - POR VALIDAR - - - - - Sumário Submetido: - - {item.sumario ? item.sumario : "O aluno não escreveu sumário para este dia."} - - - - - lidarComPresenca(item.aluno_id, item.data, 'rejeitado')} - > - - Rejeitar - - - lidarComPresenca(item.aluno_id, item.data, 'aprovado')} - > - - Aprovar - - - - - )) - )} - - )} - - ); -} - -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' } -}); \ No newline at end of file diff --git a/app/Professor/Alunos/CalendarioPresencas.tsx b/app/Professor/Alunos/CalendarioPresencas.tsx index 2994fd8..6637d48 100644 --- a/app/Professor/Alunos/CalendarioPresencas.tsx +++ b/app/Professor/Alunos/CalendarioPresencas.tsx @@ -1,10 +1,10 @@ // app/(Professor)/HistoricoPresencas.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, - Alert, + Animated, FlatList, Linking, Platform, @@ -42,25 +42,43 @@ const HistoricoPresencas = memo(() => { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); + // ESTADOS DO TOAST + 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 idStr = Array.isArray(params.alunoId) ? params.alunoId[0] : params.alunoId; const nomeStr = Array.isArray(params.nome) ? params.nome[0] : params.nome; const azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', azul: azulEPVC, laranja: laranjaEPVC, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - vermelho: '#EF4444', - verde: '#10B981', + vermelho: erroCor, + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', + verde: sucessoCor, + verdeSuave: isDarkMode ? 'rgba(16, 185, 129, 0.15)' : '#DCFCE7', }), [isDarkMode]); + const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'error') => { + 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]); + const fetchHistorico = async () => { try { setLoading(true); @@ -70,7 +88,6 @@ const HistoricoPresencas = memo(() => { return; } - // 1. Procurar o estágio deste aluno const { data: estagioData, error: errEstagio } = await supabase .from('estagios') .select('data_inicio, data_fim') @@ -86,7 +103,6 @@ const HistoricoPresencas = memo(() => { const estagio = estagioData[0]; - // 2. Buscar presenças no intervalo do estágio const { data: presencasData, error: errPresencas } = await supabase .from('presencas') .select('*') @@ -101,7 +117,7 @@ const HistoricoPresencas = memo(() => { } catch (error: any) { console.error("Erro Geral:", error); - Alert.alert("Erro", "Falha ao carregar dados do estágio."); + showToast("Falha ao carregar dados do estágio.", 'error'); } finally { setLoading(false); setRefreshing(false); @@ -119,10 +135,12 @@ const HistoricoPresencas = memo(() => { const handleNavigation = (item: Presenca) => { if (item.estado === 'faltou') { - router.push({ - pathname: '/Professor/Alunos/Faltas', - params: { alunoId: idStr, nome: nomeStr } - }); + if (item.justificacao_url) { + Linking.openURL(item.justificacao_url); + } else { + // 🟢 AVISO MODERNO USADO AQUI + showToast("Não existe justificação anexada a esta falta.", 'info'); + } } else { router.push({ pathname: '/Professor/Alunos/Sumarios', @@ -143,25 +161,43 @@ const HistoricoPresencas = memo(() => { return ( - - + + + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + - {/* HEADER */} router.back()} > - + Histórico - {nomeStr} + + {nomeStr} + @@ -174,7 +210,8 @@ const HistoricoPresencas = memo(() => { item.id} - contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 40 }]} + showsVerticalScrollIndicator={false} refreshControl={} ListHeaderComponent={() => ( @@ -187,51 +224,63 @@ const HistoricoPresencas = memo(() => { const isPresente = item.estado === 'presente'; const dataObj = new Date(item.data); + const bgColor = isPresente ? cores.verdeSuave : cores.vermelhoSuave; + const mainColor = isPresente ? cores.verde : cores.vermelho; + return ( handleNavigation(item)} style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]} > - - + + {dataObj.getDate()} - + {dataObj.toLocaleDateString('pt-PT', { month: 'short' }).replace('.', '')} - + {isPresente ? 'Presença Registada' : 'Falta Marcada'} - {isPresente ? 'Ver sumário do dia' : 'Ver justificação'} + {isPresente + ? 'Ver sumário inserido' + : (item.justificacao_url ? 'Abrir justificação anexada' : 'Falta sem justificação') + } - {item.lat && item.lng && ( + {item.lat && item.lng ? ( abrirMapa(item.lat!, item.lng!)} style={[styles.iconBtn, { backgroundColor: cores.azulSuave }]} + activeOpacity={0.6} > + ) : null} + {(!isPresente && !item.justificacao_url) ? null : ( + )} - ); }} ListEmptyComponent={() => ( - - Sem registos este ano. + + + + Nenhum registo efetuado. + As presenças/faltas irão aparecer aqui. )} /> @@ -243,27 +292,40 @@ const HistoricoPresencas = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, maxWidth: 200 }, + // 🟢 TOAST STYLES + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 100, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, + toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 10, flex: 1 }, + + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - listPadding: { paddingHorizontal: 24, paddingTop: 10 }, + + listPadding: { paddingHorizontal: 20, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, marginTop: 10 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 }, - dateBox: { width: 54, height: 54, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10, shadowOffset: { width: 0, height: 2 } }, + dateBox: { width: 56, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, dateDay: { fontSize: 18, fontWeight: '900' }, dateMonth: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' }, - infoArea: { flex: 1, marginLeft: 15 }, - statusRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 2 }, + + infoArea: { flex: 1, marginLeft: 16 }, + statusRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 4 }, indicator: { width: 8, height: 8, borderRadius: 4 }, statusText: { fontSize: 15, fontWeight: '800', letterSpacing: -0.3 }, subInfo: { fontSize: 12, fontWeight: '600' }, - actionIcons: { flexDirection: 'row', alignItems: 'center', gap: 10 }, - iconBtn: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + + actionIcons: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingRight: 5 }, + iconBtn: { width: 34, height: 34, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + emptyContainer: { marginTop: 100, alignItems: 'center' }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, + emptyText: { fontSize: 15, fontWeight: '700' }, + emptySubText: { fontSize: 13, fontWeight: '600', marginTop: 4 } }); export default HistoricoPresencas; \ No newline at end of file diff --git a/app/Professor/Alunos/CriarAluno.tsx b/app/Professor/Alunos/CriarAluno.tsx deleted file mode 100644 index 68b1d64..0000000 --- a/app/Professor/Alunos/CriarAluno.tsx +++ /dev/null @@ -1,342 +0,0 @@ -// app/Professor/Alunos/CriarAluno.tsx -import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { useEffect, useMemo, useState } from 'react'; -import { - ActivityIndicator, - Alert, - KeyboardAvoidingView, - Platform, - ScrollView, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - View, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { supabase } from '../../../lib/supabase'; -import { useTheme } from '../../../themecontext'; - -// --- FUNÇÕES DE DATA --- -const formatarDataParaDB = (dataUI: string) => { - if (!dataUI || dataUI.length !== 10) return null; - const parts = dataUI.split('-'); - if (parts.length !== 3) return null; - return `${parts[2]}-${parts[1]}-${parts[0]}`; -}; - -const aplicarMascaraData = (text: string) => { - const cleaned = text.replace(/\D/g, ''); - let formatted = cleaned; - if (cleaned.length > 2 && cleaned.length <= 4) { - formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`; - } else if (cleaned.length > 4) { - formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`; - } - return formatted; -}; - -const calcularIdade = (dataPT: string): string => { - if (!dataPT || dataPT.length !== 10) return ''; - const parts = dataPT.split('-'); - const dia = parseInt(parts[0], 10); - const mes = parseInt(parts[1], 10); - const ano = parseInt(parts[2], 10); - - if (ano < 1950 || ano > new Date().getFullYear()) return ''; - - const hoje = new Date(); - const nascimento = new Date(ano, mes - 1, dia); - - if (nascimento.getDate() !== dia) return ''; - - let idade = hoje.getFullYear() - nascimento.getFullYear(); - const m = hoje.getMonth() - nascimento.getMonth(); - if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) idade--; - return idade >= 0 ? idade.toString() : ''; -}; - -const CriarAluno = () => { - const { isDarkMode } = useTheme(); - const router = useRouter(); - const [loading, setLoading] = useState(false); - - // ESTADOS DE LOGIN - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno'); - - // ESTADOS DE PERFIL (Utilizador / Tutor) - const [nome, setNome] = useState(''); - const [residencia, setResidencia] = useState(''); - const [telefone, setTelefone] = useState(''); - const [dataNascimento, setDataNascimento] = useState(''); // Formato DD-MM-AAAA - const [idade, setIdade] = useState(''); - - // ESTADOS ESPECÍFICOS (Aluno / Professor) - const [ano, setAno] = useState(''); - const [nEscola, setNEscola] = useState(''); - const [curso, setCurso] = useState(''); - - // CAMPOS PARA EMPRESA - const [nomeEmpresa, setNomeEmpresa] = useState(''); - const [nif, setNif] = useState(''); - const [setor, setSetor] = useState(''); - - useEffect(() => { - const novaIdade = calcularIdade(dataNascimento); - if (novaIdade) setIdade(novaIdade); - else setIdade(''); - }, [dataNascimento]); - - const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: '#2390a6', - laranja: '#E38E00', - borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - placeholder: isDarkMode ? '#555' : '#A0AEC0' - }), [isDarkMode]); - - const handleCriar = async () => { - const emailLimpo = email.trim(); - - if (!emailLimpo || !password || !nome) { - Alert.alert("Atenção", "Obrigatório: Email, Password e Nome."); - return; - } - - if (tipo === 'empresa' && (!nomeEmpresa || !nif)) { - Alert.alert("Atenção", "Nome da Entidade e NIF são obrigatórios para Empresas."); - return; - } - - setLoading(true); - - try { - const { data: authData, error: authError } = await supabase.auth.signUp({ - email: emailLimpo, - password, - options: { - data: { nome, tipo }, - emailRedirectTo: undefined - } - }); - - if (authError) throw authError; - const user = authData.user; - - if (!user) { - Alert.alert("Verificação", "Utilizador criado. Verifique o email para ativar a conta."); - router.back(); - return; - } - - const dataFormatadaDB = formatarDataParaDB(dataNascimento); - - const { error: profileError } = await supabase - .from('profiles') - .insert([{ - id: user.id, - nome, // É o nome da pessoa (Aluno, Prof, ou Tutor) - email: emailLimpo, - residencia, - telefone, - idade: idade ? parseInt(idade) : null, - data_nascimento: dataFormatadaDB, - tipo, - n_escola: tipo === 'aluno' ? nEscola : null, // Apenas para aluno - curso: tipo === 'aluno' || tipo === 'professor' ? curso : null - }]); - - if (profileError) throw profileError; - - if (tipo === 'aluno') { - const { error: alunoError } = await supabase - .from('alunos') - .insert([{ - id: user.id, - nome, - n_escola: nEscola, - ano: ano ? parseInt(ano) : null, - turma_curso: curso.toUpperCase() - }]); - if (alunoError) throw alunoError; - } - - if (tipo === 'empresa') { - const { error: empresaError } = await supabase - .from('empresas') - .insert([{ - nome: nomeEmpresa, - nif: nif, - setor: setor, - tutor_nome: nome, // O nome do perfil é o nome do tutor - tutor_telefone: telefone, // O telefone do perfil é o contacto do tutor - user_id: user.id - }]); - if (empresaError) throw empresaError; - } - - Alert.alert("Sucesso", "Novo registo concluído com sucesso!"); - router.back(); - - } catch (err: any) { - Alert.alert("Erro ao criar conta", err.message); - } finally { - setLoading(false); - } - }; - - return ( - - - - - - router.back()} style={[styles.backBtn, { borderColor: cores.borda }]}> - - - Novo Registo - - - - - - {(['aluno', 'professor', 'empresa'] as const).map((item) => ( - setTipo(item)} - > - - {item.toUpperCase()} - - - ))} - - - - - - - - - - - - setDataNascimento(aplicarMascaraData(t))} placeholder="Nascimento (DD-MM-AAAA)" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder} - /> - - {idade ? `${idade} anos` : 'Idade'} - - - - - - - {tipo === 'aluno' && ( - <> - - - - - - - - )} - - {tipo === 'professor' && ( - <> - - - - )} - - {tipo === 'empresa' && ( - <> - - - - - - - - )} - - - - {loading ? : REGISTAR NO SISTEMA} - - - - - - - ); -}; - -const SectionHeader = ({ title, cores }: any) => ( - {title} -); - -const styles = StyleSheet.create({ - scroll: { padding: 24, paddingBottom: 80 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15, gap: 15 }, - backBtn: { width: 45, height: 45, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, - title: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, - sectionTitle: { fontSize: 11, fontWeight: '900', marginTop: 25, marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }, - selectorContainer: { flexDirection: 'row', gap: 10, marginBottom: 10 }, - selectorBtn: { flex: 1, height: 48, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, - form: { gap: 10 }, - input: { height: 55, borderRadius: 16, borderWidth: 1, paddingHorizontal: 18, fontSize: 15, fontWeight: '600' }, - submitBtn: { height: 62, borderRadius: 20, marginTop: 40, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, - submitBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', letterSpacing: 1 }, -}); - -export default CriarAluno; \ No newline at end of file diff --git a/app/Professor/Alunos/CriarRegisto.tsx b/app/Professor/Alunos/CriarRegisto.tsx new file mode 100644 index 0000000..2f9bfb9 --- /dev/null +++ b/app/Professor/Alunos/CriarRegisto.tsx @@ -0,0 +1,473 @@ +// app/Professor/Alunos/CriarAluno.tsx +import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { + ActivityIndicator, + Animated, + KeyboardAvoidingView, + Platform, + ScrollView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { supabase } from '../../../lib/supabase'; +import { useTheme } from '../../../themecontext'; + +const formatarDataParaDB = (dataUI: string) => { + if (!dataUI || dataUI.length !== 10) return null; + const parts = dataUI.split('-'); + if (parts.length !== 3) return null; + return `${parts[2]}-${parts[1]}-${parts[0]}`; +}; + +const aplicarMascaraData = (text: string) => { + const cleaned = text.replace(/\D/g, ''); + let formatted = cleaned; + if (cleaned.length > 2 && cleaned.length <= 4) { + formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`; + } else if (cleaned.length > 4) { + formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`; + } + return formatted; +}; + +const calcularIdade = (dataPT: string): string => { + if (!dataPT || dataPT.length !== 10) return ''; + const parts = dataPT.split('-'); + const dia = parseInt(parts[0], 10); + const mes = parseInt(parts[1], 10); + const ano = parseInt(parts[2], 10); + + if (ano < 1950 || ano > new Date().getFullYear()) return ''; + + const hoje = new Date(); + const nascimento = new Date(ano, mes - 1, dia); + + if (nascimento.getDate() !== dia) return ''; + + let idade = hoje.getFullYear() - nascimento.getFullYear(); + const m = hoje.getMonth() - nascimento.getMonth(); + if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) idade--; + return idade >= 0 ? idade.toString() : ''; +}; + +const CriarAluno = () => { + const { isDarkMode } = useTheme(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const [loading, setLoading] = useState(false); + + // ESTADOS DO TOAST NOTIFICATION + 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; + + // ESTADOS COMUNS A TODOS + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno'); + + // ESTADOS DO PERFIL/TUTOR + const [nome, setNome] = useState(''); + const [residencia, setResidencia] = useState(''); + const [telefone, setTelefone] = useState(''); + const [dataNascimento, setDataNascimento] = useState(''); + const [idade, setIdade] = useState(''); + + // ESTADOS ESCOLARES / CURSO + const [ano, setAno] = useState(''); + const [nEscola, setNEscola] = useState(''); + const [curso, setCurso] = useState(''); + + // ESTADOS ESPECÍFICOS EMPRESA + const [nomeEmpresa, setNomeEmpresa] = useState(''); + const [nif, setNif] = useState(''); + + useEffect(() => { + const novaIdade = calcularIdade(dataNascimento); + if (novaIdade) setIdade(novaIdade); + else setIdade(''); + }, [dataNascimento]); + + 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', + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + placeholder: isDarkMode ? '#555' : '#94A3B8', + verde: sucessoCor, + vermelho: erroCor + }), [isDarkMode]); + + const showToast = (message: string, type: 'error' | 'success' | 'info' = 'error') => { + 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); + }); + }; + + const handleCriar = async () => { + const emailLimpo = email.trim(); + + if (!emailLimpo || !password || !nome) { + showToast("Preencha todos os campos obrigatórios!", 'error'); + return; + } + + if (tipo === 'empresa' && (!nomeEmpresa || !curso)) { + showToast("O Nome da Empresa e o Curso são obrigatórios!", 'error'); + return; + } + + if (tipo === 'aluno' && (!nEscola || !curso || !ano)) { + showToast("Os dados escolares do aluno estão incompletos!", 'error'); + return; + } + + setLoading(true); + + try { + // 1. Criar a conta na Autenticação + const { data: authData, error: authError } = await supabase.auth.signUp({ + email: emailLimpo, + password, + options: { + data: { nome, tipo }, + emailRedirectTo: undefined + } + }); + + if (authError) throw authError; + const user = authData.user; + + if (!user) { + showToast("Novo utilizador criado!", 'info'); + setTimeout(() => router.back(), 2000); + return; + } + + // 2. Criar o Perfil Geral + const dataFormatadaDB = tipo !== 'empresa' ? formatarDataParaDB(dataNascimento) : null; + const { error: profileError } = await supabase + .from('profiles') + .insert([{ + id: user.id, + nome, + email: emailLimpo, + residencia, + telefone, + idade: (idade && tipo !== 'empresa') ? parseInt(idade) : null, + data_nascimento: dataFormatadaDB, + tipo, + n_escola: tipo === 'aluno' || tipo === 'professor' ? nEscola : null, + curso: curso ? curso.toUpperCase() : null + }]); + + if (profileError) throw profileError; + + // 3. Criar a Entidade Específica + if (tipo === 'aluno') { + const { error: alunoError } = await supabase + .from('alunos') + .insert([{ + profile_id: user.id, // 🟢 CORREÇÃO AQUI: perfil_id igual ao do teu diagrama + nome, + n_escola: nEscola, + ano: ano ? parseInt(ano) : null, + turma_curso: curso.toUpperCase() + }]); + if (alunoError) throw alunoError; + } + + if (tipo === 'empresa') { + const { error: empresaError } = await supabase + .from('empresas') + .insert([{ + nome: nomeEmpresa, + nif: nif, + morada: residencia, + 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; + } + + showToast("Registo criado com sucesso e adicionado à lista!", 'success'); + setTimeout(() => router.back(), 1500); + + } catch (err: any) { + showToast(err.message || "Erro ao criar o registo.", 'error'); + } finally { + setLoading(false); + } + }; + + return ( + + + + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + + + + + router.back()} style={[styles.backBtn, { borderColor: cores.borda, backgroundColor: cores.card }]}> + + + + Novo Registo + Sistema Central + + + + + + + + {(['aluno', 'professor', 'empresa'] as const).map((item) => ( + setTipo(item)} + activeOpacity={0.8} + > + + {item.toUpperCase()} + + + ))} + + + + + + + + + + + + + {/* DADOS DA EMPRESA */} + {tipo === 'empresa' && ( + + + + + + + + + + + )} + + {/* DADOS PESSOAIS / TUTOR */} + + + + + + {tipo !== 'empresa' && ( + + setDataNascimento(aplicarMascaraData(t))} placeholder="Data de Nascimento (DD-MM-AAAA)" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder} + /> + + + {idade ? `${idade} anos` : 'Idade'} + + + + )} + + + + + + + {/* DADOS ESCOLARES (ALUNO) */} + {tipo === 'aluno' && ( + + + + + + + + + + + )} + + {/* DADOS ESCOLARES (PROFESSOR) */} + {tipo === 'professor' && ( + + + + + + + + + + )} + + + + + {loading ? ( + + ) : ( + + + GRAVAR NOVO REGISTO + + )} + + + + + + + ); +}; + +const SectionHeader = ({ icon, title, cores }: any) => ( + + + + + {title} + +); + +const styles = StyleSheet.create({ + 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 }, + + scroll: { paddingHorizontal: 20, paddingTop: 10, paddingBottom: 60 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + backBtn: { width: 44, height: 44, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + title: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + subtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, + + pillSelector: { flexDirection: 'row', borderRadius: 18, padding: 5, marginBottom: 25 }, + pillBtn: { flex: 1, height: 46, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + + formContainer: { gap: 20 }, + sectionCard: { borderRadius: 28, padding: 22, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, + iconCircle: { width: 34, height: 34, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginRight: 12 }, + sectionTitleTxt: { fontSize: 15, fontWeight: '900', letterSpacing: -0.2 }, + + inputGroup: { gap: 12 }, + input: { height: 56, borderRadius: 16, borderWidth: 1.5, paddingHorizontal: 16, fontSize: 15, fontWeight: '700' }, + + submitBtn: { flexDirection: 'row', height: 60, borderRadius: 20, marginTop: 30, justifyContent: 'center', alignItems: 'center', elevation: 6, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.3, shadowRadius: 12 }, + submitBtnText: { color: '#fff', fontSize: 15, fontWeight: '900', letterSpacing: 1 }, +}); + +export default CriarAluno; \ No newline at end of file diff --git a/app/Professor/Alunos/DetalhesAluno.tsx b/app/Professor/Alunos/DetalhesAluno.tsx index 505264c..2301728 100644 --- a/app/Professor/Alunos/DetalhesAluno.tsx +++ b/app/Professor/Alunos/DetalhesAluno.tsx @@ -1,10 +1,10 @@ // app/Professor/Alunos/DetalhesAluno.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, - Alert, + Animated, Linking, Modal, ScrollView, @@ -19,7 +19,6 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; -// --- UTILITÁRIOS DE DATA --- const formatarDataParaUI = (dataDB: string) => { if (!dataDB) return ''; const parts = dataDB.split('-'); @@ -57,7 +56,6 @@ const aplicarMascaraData = (text: string) => { return formatted; }; -// --- TIPAGENS --- interface AlunoEditForm { nome: string; n_escola: string; @@ -80,22 +78,42 @@ const DetalhesAlunos = memo(() => { const [modalVisible, setModalVisible] = useState(false); const [saving, setSaving] = useState(false); + 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 [editForm, setEditForm] = useState({ nome: '', n_escola: '', turma_curso: '', telefone: '', residencia: '', data_nascimento: '', email: '' }); + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; + const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: '#2390a6', - laranja: '#E38E00', - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + 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', - inputFundo: isDarkMode ? '#252525' : '#EDF2F7' + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + vermelho: erroCor, + verde: sucessoCor, }), [isDarkMode]); + 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]); + const fetchAluno = async () => { if (!alunoId) return; try { @@ -143,7 +161,7 @@ const DetalhesAlunos = memo(() => { } catch (err: any) { console.error(err); - Alert.alert("Erro", "Falha ao carregar dados."); + showToast("Falha ao carregar os dados do aluno.", "error"); } finally { setLoading(false); } @@ -156,13 +174,11 @@ const DetalhesAlunos = memo(() => { const dataFormatadaDB = formatarDataParaDB(editForm.data_nascimento); const novaIdade = calcularIdade(dataFormatadaDB || ''); - // Não enviamos o 'n_escola' para garantir que os dados institucionais não são tocados const { data: d1, error: err1 } = await supabase.from('alunos').update({ nome: editForm.nome, turma_curso: editForm.turma_curso }).eq('id', alunoId).select(); - // Não enviamos o 'email' pelo mesmo motivo const { data: d2, error: err2 } = await supabase.from('profiles').update({ telefone: editForm.telefone, residencia: editForm.residencia, @@ -174,14 +190,14 @@ const DetalhesAlunos = memo(() => { if (err2) throw err2; if (!d1 || d1.length === 0 || !d2 || d2.length === 0) { - throw new Error("Permissão Negada: O professor não tem direitos (RLS) para editar este aluno no Supabase."); + throw new Error("Não tens permissões para editar este aluno."); } - Alert.alert("Sucesso", "Dados atualizados com sucesso!"); + showToast("Dados atualizados com sucesso!", "success"); setModalVisible(false); fetchAluno(); } catch (err: any) { - Alert.alert("Erro na Gravação", err.message || "Não foi possível guardar. Verifica a ligação."); + showToast(err.message || "Não foi possível guardar as alterações.", "error"); } finally { setSaving(false); } @@ -197,17 +213,35 @@ const DetalhesAlunos = memo(() => { return ( - - + + + + + {toast.message} + + + - {/* HEADER */} - router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}> + router.back()} style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}> - Ficha do Aluno - setModalVisible(true)} style={[styles.btnAction, { borderColor: cores.borda }]}> - + + Ficha do Aluno + Estágios+ + + setModalVisible(true)} style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}> + @@ -219,31 +253,32 @@ const DetalhesAlunos = memo(() => { {aluno?.nome} - {aluno?.turma_curso} + + + {aluno?.turma_curso} + - {/* INFORMAÇÕES PESSOAIS */} - + Linking.openURL(`mailto:${aluno.perfil.email}`) : null} /> Linking.openURL(`tel:${aluno.perfil.telefone}`) : null} /> - + - {/* ESTÁGIO */} Plano de Estágio @@ -258,7 +293,7 @@ const DetalhesAlunos = memo(() => { {aluno?.estagio?.empresas?.nome || 'Empresa'} - TUTOR + TUTOR RESPONSÁVEL {aluno?.estagio?.empresas?.tutor_nome || 'N/A'} {aluno?.estagio?.empresas?.tutor_telefone || '-'} @@ -272,41 +307,50 @@ const DetalhesAlunos = memo(() => { - INÍCIO + DATA DE INÍCIO {formatarDataParaUI(aluno?.estagio?.data_inicio) || '-'} - FIM + DATA DE FIM {formatarDataParaUI(aluno?.estagio?.data_fim) || '-'} ) : ( - Sem estágio atribuído + + Nenhum estágio atribuído no momento )} - {/* MODAL DE EDIÇÃO */} + - Editar Aluno - setModalVisible(false)}> - + + Editar Aluno + Atualizar Informações + + setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> + - + setEditForm({...editForm, nome: t})} cores={cores} /> - {/* CAMPOS INSTITUCIONAIS BLOQUEADOS (editable={false}) */} - - + + + + + + setEditForm({...editForm, turma_curso: t})} cores={cores} /> + + - setEditForm({...editForm, turma_curso: t})} cores={cores} /> + setEditForm({...editForm, telefone: t})} cores={cores} keyboard="phone-pad" /> { ); }); -// --- COMPONENTES AUXILIARES --- interface EditInputProps { label: string; value: string; @@ -343,15 +386,17 @@ interface EditInputProps { } const EditInput = ({ label, value, onChange, cores, keyboard = "default", maxLength, editable = true }: EditInputProps) => ( - - {label} + + {label} ( - - - - {label} - {value || '-'} + + + - {onPress && } + + {label} + {value || 'Não definido'} + + {onPress && } ); const styles = StyleSheet.create({ centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 18, fontWeight: '900' }, - btnAction: { width: 44, height: 44, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - scroll: { paddingHorizontal: 24, paddingTop: 10 }, - profileSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 25, gap: 18 }, - avatar: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center' }, - avatarTxt: { fontSize: 26, fontWeight: '900' }, - alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - alunoCurso: { fontSize: 13, fontWeight: '800' }, - infoCard: { borderRadius: 25, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 5, marginBottom: 30 }, - row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 }, - rowLabel: { fontSize: 9, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 }, - rowValue: { fontSize: 14, fontWeight: '700' }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, - sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginRight: 15 }, - sectionLine: { flex: 1, height: 1, opacity: 0.2 }, - estagioCard: { padding: 25, borderRadius: 30 }, + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, 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 }, + headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + + scroll: { paddingHorizontal: 24, paddingTop: 15 }, + + profileSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 30, gap: 16 }, + avatar: { width: 70, height: 70, borderRadius: 24, justifyContent: 'center', alignItems: 'center' }, + avatarTxt: { fontSize: 28, fontWeight: '900' }, + alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5, marginBottom: 6 }, + roleBadge: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + alunoCurso: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + + infoCard: { borderRadius: 28, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 8, marginBottom: 35, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 }, + row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14 }, + iconBox: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 12 }, + rowLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2, letterSpacing: 0.5 }, + rowValue: { fontSize: 15, fontWeight: '700' }, + + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 }, + sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8, marginRight: 15 }, + sectionLine: { flex: 1, height: 1, opacity: 0.4 }, + + estagioCard: { padding: 24, borderRadius: 28, elevation: 4, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.3, shadowRadius: 12 }, estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 }, - statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 }, - statusText: { color: '#fff', fontSize: 9, fontWeight: '900' }, - empresaNome: { color: '#fff', fontSize: 22, fontWeight: '900', marginBottom: 15 }, - tutorInfo: { marginBottom: 15 }, - miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 8, fontWeight: '900' }, - tutorNome: { color: '#fff', fontSize: 15, fontWeight: '800' }, - tutorTel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 }, - horarioBox: { backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15, marginBottom: 15 }, - horarioTxt: { color: '#fff', fontSize: 13, fontWeight: '700' }, - estagioFooter: { flexDirection: 'row', justifyContent: 'space-between' }, + statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + statusText: { color: '#fff', fontSize: 10, fontWeight: '900', letterSpacing: 0.5 }, + empresaNome: { color: '#fff', fontSize: 22, fontWeight: '900', marginBottom: 18, letterSpacing: -0.5 }, + tutorInfo: { marginBottom: 18 }, + miniLabel: { color: 'rgba(255,255,255,0.6)', fontSize: 9, fontWeight: '900', letterSpacing: 0.5, marginBottom: 2 }, + tutorNome: { color: '#fff', fontSize: 16, fontWeight: '800' }, + tutorTel: { color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: '600' }, + horarioBox: { backgroundColor: 'rgba(255,255,255,0.1)', padding: 16, borderRadius: 18, marginBottom: 18 }, + horarioTxt: { color: '#fff', fontSize: 14, fontWeight: '700' }, + estagioFooter: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 }, periodoCol: { flex: 1 }, - footerVal: { color: '#fff', fontSize: 13, fontWeight: '800' }, - noEstagio: { padding: 20, borderRadius: 20, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center' }, - noEstagioTxt: { fontWeight: '700', fontSize: 14 }, + footerVal: { color: '#fff', fontSize: 14, fontWeight: '800', marginTop: 2 }, + + noEstagio: { padding: 30, borderRadius: 24, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center', justifyContent: 'center' }, + noEstagioTxt: { fontWeight: '700', fontSize: 15 }, + modalContainer: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, - modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 25, maxHeight: '90%' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - modalTitle: { fontSize: 20, fontWeight: '900' }, - input: { borderRadius: 12, padding: 12, fontSize: 15, fontWeight: '600' }, - btnSave: { borderRadius: 15, padding: 16, alignItems: 'center', marginTop: 20, marginBottom: 40 }, - btnSaveTxt: { color: '#fff', fontWeight: '900', fontSize: 16 } + modalContent: { borderTopLeftRadius: 36, borderTopRightRadius: 36, padding: 25, maxHeight: '92%' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 25 }, + modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + modalSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, + closeBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + + inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, + input: { borderRadius: 16, paddingHorizontal: 16, height: 52, fontSize: 15, fontWeight: '700' }, + btnSave: { borderRadius: 18, height: 60, justifyContent: 'center', alignItems: 'center', marginTop: 20, elevation: 4, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 }, + btnSaveTxt: { color: '#fff', fontWeight: '900', fontSize: 16, letterSpacing: 0.5 } }); export default DetalhesAlunos; \ No newline at end of file diff --git a/app/Professor/Alunos/Estagios.tsx b/app/Professor/Alunos/Estagios.tsx index 53cb716..466de0d 100644 --- a/app/Professor/Alunos/Estagios.tsx +++ b/app/Professor/Alunos/Estagios.tsx @@ -1,9 +1,10 @@ // app/Professor/Estagios/index.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, + Animated, Modal, ScrollView, StatusBar, @@ -18,7 +19,7 @@ import { supabase } from '../../../lib/supabase'; import { useTheme } from '../../../themecontext'; // --- Interfaces --- -interface Aluno { id: string; nome: string; turma_curso: string; ano: number; } +interface Aluno { id: string; nome: string; turma_curso: string; ano: number; n_escola?: string; } interface Empresa { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; } interface Estagio { id: string; @@ -49,20 +50,24 @@ export default function Estagios() { const azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', azul: azulEPVC, laranja: laranjaEPVC, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', - vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.12)' : '#FFF5F5', - laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.12)' : '#FFF9F0', - vermelho: '#EF4444', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', + laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6', + verde: sucessoCor, + vermelho: erroCor, borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - overlay: 'rgba(26, 54, 93, 0.8)', + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + overlay: 'rgba(0, 0, 0, 0.6)', }), [isDarkMode]); // Estados @@ -73,7 +78,11 @@ export default function Estagios() { const [modalVisible, setModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); - // ESTADO DO ALERTA CUSTOMIZADO (AGORA CENTRADO E BONITO) + // 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; + + // ESTADO DO ALERTA CUSTOMIZADO const [customAlert, setCustomAlert] = useState({ visible: false, title: '', msg: '', tipo: 'warning' as 'warning' | 'error' }); const [estagioParaApagar, setEstagioParaApagar] = useState<{id: string, nome: string} | null>(null); @@ -88,12 +97,26 @@ export default function Estagios() { const [horasTotaisEstagio, setHorasTotaisEstagio] = useState(''); const [searchMain, setSearchMain] = useState(''); + // Estados de Pesquisa do Modal + const [pesquisaAluno, setPesquisaAluno] = useState(''); + const [pesquisaEmpresa, setPesquisaEmpresa] = useState(''); + // Horários Diários const [hManhaIni, setHManhaIni] = useState(''); const [hManhaFim, setHManhaFim] = useState(''); const [hTardeIni, setHTardeIni] = useState(''); const [hTardeFim, setHTardeFim] = useState(''); + 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]); + const showCustomAlert = (title: string, msg: string, tipo: 'warning' | 'error' = 'warning') => { setCustomAlert({ visible: true, title, msg, tipo }); }; @@ -105,13 +128,18 @@ export default function Estagios() { setLoading(true); const [resEstagios, resAlunos, resEmpresas] = await Promise.all([ supabase.from('estagios').select('*, alunos(nome, turma_curso, ano), empresas(*)'), - supabase.from('alunos').select('id, nome, turma_curso, ano').order('nome'), + supabase.from('alunos').select('id, nome, turma_curso, ano, n_escola').order('nome'), supabase.from('empresas').select('*').order('nome') ]); if (resEstagios.data) setEstagios(resEstagios.data); if (resAlunos.data) setAlunos(resAlunos.data); if (resEmpresas.data) setEmpresas(resEmpresas.data); - } catch (e) { console.error(e); } finally { setLoading(false); } + } catch (e) { + console.error(e); + showToast("Erro ao carregar os dados", "error"); + } finally { + setLoading(false); + } }; const carregarHorariosEdicao = async (estagioId: string) => { @@ -130,6 +158,14 @@ export default function Estagios() { } }; + // --- FUNÇÕES DE MÁSCARA --- + const mascararData = (text: string) => { + const num = text.replace(/\D/g, ''); + if (num.length > 6) return `${num.slice(0, 4)}-${num.slice(4, 6)}-${num.slice(6, 8)}`; + if (num.length > 4) return `${num.slice(0, 4)}-${num.slice(4, 6)}`; + return num; + }; + const aplicarMascaraHora = (value: string) => { const cleaned = value.replace(/\D/g, ''); if (cleaned.length >= 3) { @@ -138,6 +174,19 @@ export default function Estagios() { return cleaned; }; + // --- FUNÇÃO PARA HORÁRIOS RÁPIDOS --- + const preencherHorario = (tipo: 'laboral' | 'manha' | 'tarde' | 'limpar') => { + if (tipo === 'laboral') { + setHManhaIni('09:00'); setHManhaFim('13:00'); setHTardeIni('14:00'); setHTardeFim('18:00'); + } else if (tipo === 'manha') { + setHManhaIni('09:00'); setHManhaFim('13:00'); setHTardeIni(''); setHTardeFim(''); + } else if (tipo === 'tarde') { + setHManhaIni(''); setHManhaFim(''); setHTardeIni('14:00'); setHTardeFim('18:00'); + } else { + setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim(''); + } + }; + const totalHorasDiarias = useMemo(() => { const calcularMinutos = (ini: string, fim: string) => { if (!ini || !fim || !ini.includes(':') || !fim.includes(':')) return 0; @@ -159,8 +208,9 @@ export default function Estagios() { return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`; }; + // --- LÓGICA MANTIDA INTACTA --- const calcularMagia = (tipo: 'dataFim' | 'horasTotais') => { - if (!dataInicio) return showCustomAlert("Atenção", "Preenche a Data de Início primeiro!"); + if (!dataInicio || dataInicio.length < 10) return showCustomAlert("Atenção", "Preenche a Data de Início completa primeiro!"); if (!hManhaIni && !hTardeIni) return showCustomAlert("Atenção", "Preenche primeiro o horário diário (Manhã e/ou Tarde)!"); const matchH = totalHorasDiarias.match(/(\d+)h/); @@ -192,7 +242,7 @@ export default function Estagios() { setDataFim(currentDate.toISOString().split('T')[0]); } else if (tipo === 'horasTotais') { - if (!dataFim) return showCustomAlert("Atenção", "Preenche a Data Fim (Prev.) primeiro!"); + if (!dataFim || dataFim.length < 10) return showCustomAlert("Atenção", "Preenche a Data Fim completa primeiro!"); let currentDate = new Date(dataInicio); const endDate = new Date(dataFim); @@ -283,6 +333,7 @@ export default function Estagios() { handleFecharModal(); fetchDados(); + showToast("Plano de estágio guardado com sucesso!", "success"); } catch (error: any) { showCustomAlert("Erro ao Guardar", error.message, 'error'); } finally { @@ -294,16 +345,24 @@ export default function Estagios() { if (!estagioParaApagar) return; setLoading(true); const { error } = await supabase.from('estagios').delete().eq('id', estagioParaApagar.id); - if (!error) { setDeleteModalVisible(false); fetchDados(); } + if (!error) { + setDeleteModalVisible(false); + fetchDados(); + showToast("Estágio apagado com sucesso.", "success"); + } else { + showToast("Erro ao apagar o estágio.", "error"); + } setLoading(false); }; const handleFecharModal = () => { setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); setDataInicio(''); setDataFim(''); setHorasTotaisEstagio(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim(''); + setPesquisaAluno(''); setPesquisaEmpresa(''); setPasso(1); }; + // --- FILTROS --- const estagiosFiltrados = useMemo(() => { const groups: Record = {}; estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => { @@ -314,33 +373,63 @@ export default function Estagios() { return Object.keys(groups).map(titulo => ({ titulo, dados: groups[titulo] })).sort((a, b) => b.titulo.localeCompare(a.titulo)); }, [estagios, searchMain]); + // Filtros em tempo real para os modais + const alunosParaMostrar = useMemo(() => { + return alunos.filter(a => a.nome.toLowerCase().includes(pesquisaAluno.toLowerCase()) || (a.n_escola && a.n_escola.includes(pesquisaAluno))); + }, [alunos, pesquisaAluno]); + + const empresasParaMostrar = useMemo(() => { + return empresas.filter(e => e.nome.toLowerCase().includes(pesquisaEmpresa.toLowerCase())); + }, [empresas, pesquisaEmpresa]); + return ( - - + + + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + + {/* HEADER MODERNO */} - router.back()}> + router.back()}> Plano de Estágios {estagios.length} Alunos Colocados - + - - - + {/* SEARCH BAR MODERNA */} + + + + + {loading ? ( @@ -375,10 +464,10 @@ export default function Estagios() { > - {e.alunos?.nome.charAt(0)} + {e.alunos?.nome.charAt(0).toUpperCase()} - {e.alunos?.nome} + {e.alunos?.nome} {e.empresas?.nome} @@ -400,9 +489,10 @@ export default function Estagios() { { setPasso(1); setModalVisible(true); }} > - + Novo Plano @@ -425,57 +515,91 @@ export default function Estagios() { {passo === 1 ? ( - Selecionar Estagiário - - - {Object.entries(alunos.reduce((acc, a) => { - const key = `${a.ano}º ${a.turma_curso}`.trim().toUpperCase(); - if (!acc[key]) acc[key] = []; acc[key].push(a); return acc; - }, {} as Record)) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([turma, lista]) => ( - - {turma} - {lista.map(a => ( - setAlunoSelecionado(a)} - > - {a.nome} - {alunoSelecionado?.id === a.id && } - - ))} - - ))} - + + {/* SELECIONAR ALUNO COM PESQUISA */} + + Selecionar Estagiário + + + + + + + {Object.entries(alunosParaMostrar.reduce((acc, a) => { + const key = `${a.ano}º ${a.turma_curso}`.trim().toUpperCase(); + if (!acc[key]) acc[key] = []; acc[key].push(a); return acc; + }, {} as Record)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([turma, lista]) => ( + + {turma} + {lista.map(a => ( + setAlunoSelecionado(a)} + > + {a.nome} + {alunoSelecionado?.id === a.id && } + + ))} + + ))} + {alunosParaMostrar.length === 0 && ( + Sem resultados. + )} + + - Entidade de Acolhimento - - - {Object.entries(empresas.reduce((acc, e) => { - const key = (e.curso || 'GERAL').trim().toUpperCase(); - if (!acc[key]) acc[key] = []; acc[key].push(e); return acc; - }, {} as Record)) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([curso, lista]) => ( - - {curso} - {lista.map(emp => ( - setEmpresaSelecionada(emp)} - > - {emp.nome} - {empresaSelecionada?.id === emp.id && } - - ))} - - ))} - + {/* SELECIONAR EMPRESA COM PESQUISA */} + + Entidade de Acolhimento + + + + + + + {Object.entries(empresasParaMostrar.reduce((acc, e) => { + const key = (e.curso || 'GERAL').trim().toUpperCase(); + if (!acc[key]) acc[key] = []; acc[key].push(e); return acc; + }, {} as Record)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([curso, lista]) => ( + + {curso} + {lista.map(emp => ( + setEmpresaSelecionada(emp)} + > + {emp.nome} + {empresaSelecionada?.id === emp.id && } + + ))} + + ))} + {empresasParaMostrar.length === 0 && ( + Sem resultados. + )} + + + ) : ( @@ -485,7 +609,15 @@ export default function Estagios() { DATA INÍCIO - + setDataInicio(mascararData(t))} + placeholder="AAAA-MM-DD" + maxLength={10} + keyboardType="numeric" + placeholderTextColor={cores.secundario} + /> @@ -494,7 +626,15 @@ export default function Estagios() { - + setDataFim(mascararData(t))} + placeholder="AAAA-MM-DD" + maxLength={10} + keyboardType="numeric" + placeholderTextColor={cores.secundario} + /> @@ -507,10 +647,11 @@ export default function Estagios() { @@ -521,41 +662,59 @@ export default function Estagios() { Horário de Trabalho {totalHorasDiarias}/dia + + {/* BOTÕES DE PREENCHIMENTO RÁPIDO */} + + preencherHorario('laboral')}> + Laboral (9-18h) + + preencherHorario('manha')}> + Manhã (9-13h) + + preencherHorario('limpar')}> + + + + Manhã setHManhaIni(aplicarMascaraHora(txt))} keyboardType="numeric" maxLength={5} + placeholderTextColor={cores.secundario} placeholder="09:00" /> setHManhaFim(aplicarMascaraHora(txt))} keyboardType="numeric" maxLength={5} + placeholderTextColor={cores.secundario} placeholder="13:00" /> Tarde setHTardeIni(aplicarMascaraHora(txt))} keyboardType="numeric" maxLength={5} + placeholderTextColor={cores.secundario} placeholder="14:00" /> setHTardeFim(aplicarMascaraHora(txt))} keyboardType="numeric" maxLength={5} + placeholderTextColor={cores.secundario} placeholder="18:00" /> @@ -563,8 +722,8 @@ export default function Estagios() { Responsável na Empresa - setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholder="Nome do Tutor"/> - setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Telemóvel"/> + setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholderTextColor={cores.secundario} placeholder="Nome do Tutor"/> + setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholderTextColor={cores.secundario} placeholder="Telemóvel"/> )} @@ -580,12 +739,12 @@ export default function Estagios() { onPress={() => { if(passo === 1) { if(alunoSelecionado && empresaSelecionada) setPasso(2); - else showCustomAlert("Atenção", "Selecione o aluno e a empresa antes de avançar."); + else showToast("Selecione o aluno e a empresa antes de avançar.", "info"); } else salvarEstagio(); }} style={[styles.btnModalPri, { backgroundColor: cores.azul }]} > - {loading ? : {passo === 1 ? "CONTINUAR" : "FINALIZAR"}} + {loading ? : {passo === 1 ? "CONTINUAR" : "FINALIZAR"}} @@ -602,7 +761,6 @@ export default function Estagios() { - {/* Botão de fechar (X vermelho pequeno) centrado no topo */} setCustomAlert({ ...customAlert, visible: false })} @@ -619,7 +777,6 @@ export default function Estagios() { {customAlert.title.replace(' ⚖️', '')} - {/* Mostra a balança apenas se for o erro do Limite Legal */} {customAlert.title.includes('Excedido') && ( ⚖️ )} @@ -632,7 +789,7 @@ export default function Estagios() { - {/* DELETE MODAL ORIGINAL */} + {/* DELETE MODAL (COM NOVO DESIGN ARREDONDADO) */} @@ -644,8 +801,8 @@ export default function Estagios() { Irá remover o vínculo de {estagioParaApagar?.nome}. Esta ação é irreversível. - setDeleteModalVisible(false)}> - FECHAR + setDeleteModalVisible(false)}> + CANCELAR ELIMINAR @@ -660,103 +817,88 @@ export default function Estagios() { } const styles = StyleSheet.create({ - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + // 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 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - scroll: { paddingHorizontal: 24, paddingTop: 10 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5, marginBottom: 25 }, - searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + + scroll: { paddingHorizontal: 20, paddingTop: 10 }, + searchSection: { paddingHorizontal: 4, marginBottom: 15 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '600' }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - estagioCard: { padding: 18, borderRadius: 28, borderWidth: 1, marginBottom: 14, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + estagioCard: { padding: 16, borderRadius: 24, borderWidth: 1, marginBottom: 14, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 14 }, avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, avatarText: { fontSize: 20, fontWeight: '900' }, alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, empresaRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, empresaText: { fontSize: 13, fontWeight: '600' }, - timeBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12, borderWidth: 1 }, - timeText: { fontSize: 11, fontWeight: '900' }, - fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 10 }, + timeBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10, borderWidth: 1 }, + timeText: { fontSize: 10, fontWeight: '900' }, + + fab: { position: 'absolute', right: 20, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 6, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 10 }, fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase', letterSpacing: 0.5 }, + modalOverlay: { flex: 1, justifyContent: 'flex-end', alignItems: 'center' }, - modalContent: { borderTopLeftRadius: 45, borderTopRightRadius: 45, padding: 28, height: '92%', width: '100%' }, + modalContent: { borderTopLeftRadius: 36, borderTopRightRadius: 36, padding: 25, height: '92%', width: '100%' }, modalIndicator: { width: 45, height: 5, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, alignSelf: 'center', marginBottom: 20 }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 }, - modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, - modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 }, + modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + modalSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 4, letterSpacing: 0.5 }, closeBtn: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 10, marginLeft: 5, letterSpacing: 0.5 }, + + inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 10, marginLeft: 5, letterSpacing: 0.5 }, + modalSearchBar: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 16, paddingHorizontal: 14, height: 46, marginBottom: 12 }, pickerContainer: { borderWidth: 1.5, borderRadius: 24, overflow: 'hidden', width: '100%' }, groupHeader: { paddingVertical: 10, paddingHorizontal: 18 }, groupHeaderText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' }, pickerItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 18, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, pickerItemText: { fontSize: 15, fontWeight: '700' }, - modernGroup: { padding: 20, borderRadius: 30, borderWidth: 1, marginBottom: 18 }, + + modernGroup: { padding: 20, borderRadius: 28, borderWidth: 1, marginBottom: 18 }, groupTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 0.5 }, rowInputs: { flexDirection: 'row', gap: 15 }, miniLabel: { fontSize: 9, fontWeight: '900', color: '#94A3B8', marginBottom: 6, marginLeft: 2 }, - modernInput: { paddingVertical: 14, paddingHorizontal: 16, borderRadius: 16, fontSize: 14, fontWeight: '700', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, + modernInput: { paddingVertical: 14, paddingHorizontal: 16, borderRadius: 16, fontSize: 14, fontWeight: '700', borderWidth: 1 }, + + quickTimeRow: { flexDirection: 'row', gap: 8, marginBottom: 15 }, + quickTimeBtn: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 }, + quickTimeBtnText: { fontSize: 11, fontWeight: '800' }, tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 }, tabelaLabel: { flex: 1, fontSize: 14, fontWeight: '800' }, - tabelaInput: { flex: 1, padding: 14, borderRadius: 16, textAlign: 'center', fontSize: 14, fontWeight: '800', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, + tabelaInput: { flex: 1, padding: 14, borderRadius: 16, textAlign: 'center', fontSize: 14, fontWeight: '800', borderWidth: 1 }, + totalBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, totalText: { color: '#fff', fontSize: 11, fontWeight: '900' }, + modalFooter: { flexDirection: 'row', gap: 15, marginTop: 15 }, - btnModalPri: { flex: 2, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center' }, - btnModalSec: { flex: 1, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5 }, - deleteCard: { width: '85%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center', marginBottom: 30 }, + btnModalPri: { flex: 2, height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + btnModalSec: { flex: 1, height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5 }, + + deleteCard: { width: '85%', borderRadius: 36, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center', marginBottom: 30, elevation: 10 }, deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12 }, + deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12, letterSpacing: -0.5 }, deleteSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 }, deleteFooter: { flexDirection: 'row', gap: 15, width: '100%' }, - deleteBtnCancel: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + deleteBtnCancel: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, deleteBtnText: { fontSize: 14, fontWeight: '900' }, + // --- ESTILOS ESPECÍFICOS PARA O AVISO CENTRADO --- - alertOverlay: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(26, 54, 93, 0.8)', - paddingHorizontal: 20 - }, - alertCard: { - width: '85%', - borderRadius: 30, - padding: 30, - alignItems: 'center', - borderWidth: 1, - elevation: 10, - shadowColor: '#000', - shadowOpacity: 0.15, - shadowRadius: 10, - }, - alertIconBg: { - width: 36, - height: 36, - borderRadius: 18, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 15 - }, - alertTitle: { - fontSize: 20, - fontWeight: '900', - textAlign: 'center', - marginBottom: 5 - }, - alertEmoji: { - fontSize: 28, - marginBottom: 10 - }, - alertSubtitle: { - fontSize: 14, - textAlign: 'center', - lineHeight: 22, - fontWeight: '500' - }, + alertOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.6)', paddingHorizontal: 20 }, + alertCard: { width: '85%', borderRadius: 36, padding: 30, alignItems: 'center', borderWidth: 1, elevation: 10, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 10 }, + alertIconBg: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + alertTitle: { fontSize: 20, fontWeight: '900', textAlign: 'center', marginBottom: 8, letterSpacing: -0.5 }, + alertEmoji: { fontSize: 28, marginBottom: 10 }, + alertSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '600' }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/Faltas.tsx b/app/Professor/Alunos/Faltas.tsx index 835e925..64764bc 100644 --- a/app/Professor/Alunos/Faltas.tsx +++ b/app/Professor/Alunos/Faltas.tsx @@ -1,10 +1,10 @@ // app/Professor/Alunos/Faltas.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, - Alert, + Animated, FlatList, Linking, Modal, @@ -53,23 +53,40 @@ const ListaFaltasProfessor = memo(() => { const [faltas, setFaltas] = useState([]); const [loadingFaltas, setLoadingFaltas] = useState(false); + // Estados do Toast + 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 azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Ajustado para 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.12)' : '#F0F9FA', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - vermelho: '#EF4444', - vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.12)' : '#FEF2F2', - verde: '#10B981', + vermelho: erroCor, + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', + verde: sucessoCor, + verdeSuave: isDarkMode ? 'rgba(16, 185, 129, 0.15)' : '#DCFCE7', }), [isDarkMode]); + 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]); + const fetchAlunos = async () => { try { setLoading(true); @@ -99,6 +116,7 @@ const ListaFaltasProfessor = memo(() => { ); } catch (err) { console.error(err); + showToast("Erro ao carregar as turmas.", "error"); } finally { setLoading(false); setRefreshing(false); @@ -123,7 +141,7 @@ const ListaFaltasProfessor = memo(() => { if (errEstagio) throw errEstagio; - // Se não houver estágio, não há intervalo para filtrar, logo não mostramos faltas "soltas" + // Se não houver estágio, não há intervalo para filtrar if (!estagioData) { setFaltas([]); return; @@ -143,7 +161,7 @@ const ListaFaltasProfessor = memo(() => { setFaltas(data || []); } catch (err) { console.error(err); - Alert.alert("Erro", "Falha ao carregar o histórico de faltas do estágio."); + showToast("Falha ao carregar as faltas.", "error"); } finally { setLoadingFaltas(false); } @@ -160,7 +178,7 @@ const ListaFaltasProfessor = memo(() => { try { await Linking.openURL(url); } catch (error) { - Alert.alert("Erro", "Não foi possível abrir o ficheiro."); + showToast("Não foi possível abrir o ficheiro.", "error"); } }; @@ -178,14 +196,30 @@ const ListaFaltasProfessor = memo(() => { return ( - + - + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + - {/* HEADER */} + {/* HEADER MODERNO */} router.back()} > @@ -193,24 +227,24 @@ const ListaFaltasProfessor = memo(() => { Faltas - Histórico de Assiduidade + Estágios+ - {/* SEARCH */} + {/* SEARCH SECTION MODERNA */} - + { data={filteredTurmas} keyExtractor={item => item.nome} contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + showsVerticalScrollIndicator={false} refreshControl={} renderItem={({ item }) => ( - + + {item.nome} @@ -237,7 +273,7 @@ const ListaFaltasProfessor = memo(() => { {item.alunos.map((aluno) => ( abrirFaltas(aluno)} > @@ -248,16 +284,15 @@ const ListaFaltasProfessor = memo(() => { - {aluno.nome} + {aluno.nome} - - Nº {aluno.n_escola} + + Nº {aluno.n_escola} • Gerir Faltas - - VER FALTAS + ))} @@ -265,8 +300,10 @@ const ListaFaltasProfessor = memo(() => { )} ListEmptyComponent={() => ( - - Nenhum aluno encontrado. + + + + Nenhum aluno encontrado. )} /> @@ -276,10 +313,11 @@ const ListaFaltasProfessor = memo(() => { setModalVisible(false)}> - - + + + Registo de Faltas - {alunoSelecionado?.nome} + {alunoSelecionado?.nome} setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> @@ -287,13 +325,16 @@ const ListaFaltasProfessor = memo(() => { {loadingFaltas ? ( - + ) : ( - + {faltas.length === 0 ? ( - - - Este aluno não tem faltas no estágio atual! + + + + + Nenhuma falta registada! + Este aluno tem a assiduidade em dia. ) : ( faltas.map((f) => ( @@ -305,12 +346,19 @@ const ListaFaltasProfessor = memo(() => { {new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })} - - {f.justificacao_url ? 'JUSTIFICADA' : 'INJUSTIFICADA'} - + + + + {f.justificacao_url ? 'JUSTIFICADA' : 'INJUSTIFICADA'} + + {f.justificacao_url && ( - verDocumento(f.justificacao_url!)}> + verDocumento(f.justificacao_url!)} + activeOpacity={0.7} + > )} @@ -330,39 +378,52 @@ const ListaFaltasProfessor = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + // TOAST STYLES + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, 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 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - searchSection: { paddingHorizontal: 24, marginBottom: 10 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, - searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, - listPadding: { paddingHorizontal: 24, paddingTop: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 }, + + searchSection: { paddingHorizontal: 20, marginBottom: 15 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + searchInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + + listPadding: { paddingHorizontal: 20, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, marginTop: 10 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, - avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, fontWeight: '900' }, - alunoInfo: { flex: 1, marginLeft: 15 }, - alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, - idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, - idText: { fontSize: 13, fontWeight: '600' }, - statusBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10 }, - statusText: { fontSize: 9, fontWeight: '900' }, - emptyContainer: { marginTop: 80, alignItems: 'center' }, + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, + avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 20, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5 }, + idText: { fontSize: 12, fontWeight: '700' }, + statusBadge: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, - modalContent: { height: '85%', borderTopLeftRadius: 40, borderTopRightRadius: 40, elevation: 20 }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 24 }, - modalTitle: { fontSize: 20, fontWeight: '900' }, - modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 }, - closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - faltaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1 }, - dateIcon: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - faltaData: { fontSize: 16, fontWeight: '800' }, - statusFaltaText: { fontSize: 10, fontWeight: '900', marginTop: 2 }, - btnView: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' } + modalContent: { height: '88%', borderTopLeftRadius: 32, borderTopRightRadius: 32, overflow: 'hidden' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, paddingBottom: 15, borderBottomWidth: 1 }, + modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 4 }, + closeBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + + faltaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 14, borderWidth: 1, elevation: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 5 }, + dateIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + faltaData: { fontSize: 15, fontWeight: '800', marginBottom: 6 }, + tagJustificada: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, gap: 4 }, + statusFaltaText: { fontSize: 10, fontWeight: '900', letterSpacing: 0.5 }, + btnView: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 6, shadowOffset: { width: 0, height: 3 } }, + + emptyContainer: { marginTop: 100, alignItems: 'center' }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, + emptyText: { fontSize: 15, fontWeight: '700' }, + emptySubText: { fontSize: 13, fontWeight: '600', marginTop: 4 } }); export default ListaFaltasProfessor; \ No newline at end of file diff --git a/app/Professor/Alunos/ListaAlunos.tsx b/app/Professor/Alunos/ListaAlunos.tsx index cddd442..e0f6f75 100644 --- a/app/Professor/Alunos/ListaAlunos.tsx +++ b/app/Professor/Alunos/ListaAlunos.tsx @@ -1,10 +1,10 @@ // app/Professor/Alunos/ListaAlunosProfessor.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, - Alert, + Animated, FlatList, Modal, RefreshControl, @@ -47,25 +47,45 @@ const ListaAlunosProfessor = memo(() => { const [alunoParaEliminar, setAlunoParaEliminar] = useState(null); const [isDeleting, setIsDeleting] = useState(false); + // Estados para o Toast + 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 azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; + const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: '#2390a6', - laranja: '#E38E00', - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design + 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: '#EF4444', + vermelho: erroCor, vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', + verde: sucessoCor, }), [isDarkMode]); + 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]); + const fetchAlunos = async () => { try { setLoading(true); const { data, error } = await supabase .from('alunos') - .select(`id, nome, n_escola, ano, turma_curso`) + .select('id, nome, n_escola, ano, turma_curso') .order('ano', { ascending: false }) .order('nome', { ascending: true }); @@ -89,6 +109,7 @@ const ListaAlunosProfessor = memo(() => { }))); } catch (err) { console.error("Erro ao carregar lista:", err); + showToast("Falha ao carregar a lista de alunos.", "error"); } finally { setLoading(false); setRefreshing(false); @@ -101,7 +122,6 @@ const ListaAlunosProfessor = memo(() => { try { setIsDeleting(true); - // APAGAR NO PROFILES (O Cascade do SQL trata das tabelas 'alunos' e 'estagios') const { data, error } = await supabase .from('profiles') .delete() @@ -110,12 +130,10 @@ const ListaAlunosProfessor = memo(() => { if (error) throw error; - // Se o data vier vazio, o RLS bloqueou o delete no profiles if (!data || data.length === 0) { - throw new Error("O servidor recusou apagar o perfil. Verifica se as políticas RLS na tabela 'profiles' permitem DELETE."); + throw new Error("O servidor recusou a eliminação. Verifique permissões RLS."); } - // Sucesso no banco -> Atualizar UI local setTurmas(prev => prev.map(turma => ({ ...turma, alunos: turma.alunos.filter(a => a.id !== alunoParaEliminar.id) @@ -123,11 +141,11 @@ const ListaAlunosProfessor = memo(() => { setShowDeleteModal(false); setAlunoParaEliminar(null); - Alert.alert("Sucesso", "Aluno e todos os dados vinculados foram eliminados."); + showToast("Aluno apagado com sucesso do sistema.", "success"); } catch (err: any) { console.error("ERRO AO APAGAR:", err); - Alert.alert("Erro Crítico", err.message); + showToast(err.message || "Não foi possível apagar o aluno.", "error"); } finally { setIsDeleting(false); } @@ -153,19 +171,36 @@ const ListaAlunosProfessor = memo(() => { return ( - - + + + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + - {/* HEADER */} + {/* HEADER MODERNO */} - router.back()}> + router.back()}> - Alunos - Gestão de Turmas + Lista de alunos + Estágios+ - + @@ -173,10 +208,10 @@ const ListaAlunosProfessor = memo(() => { {/* PESQUISA */} - + { item.nome} - contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 100 }]} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 40 }]} + showsVerticalScrollIndicator={false} refreshControl={} renderItem={({ item }) => ( - + {item.nome} @@ -202,7 +238,7 @@ const ListaAlunosProfessor = memo(() => { {item.alunos.map((aluno) => ( router.push({ pathname: '/Professor/Alunos/DetalhesAluno', params: { alunoId: aluno.id } })} onLongPress={() => { @@ -214,34 +250,55 @@ const ListaAlunosProfessor = memo(() => { {aluno.nome.charAt(0).toUpperCase()} - {aluno.nome} - Nº {aluno.n_escola} + {aluno.nome} + + + Nº {aluno.n_escola} + - + { + setAlunoParaEliminar(aluno); + setShowDeleteModal(true); + }} + > + + ))} )} + ListEmptyComponent={() => ( + + + + + Nenhum aluno encontrado. + + )} /> )} {/* MODAL DE ELIMINAÇÃO */} - + - Eliminar Permanentemente? + Ação Irreversível - Estás prestes a apagar o perfil de {alunoParaEliminar?.nome}. - Isto removerá o acesso à app, dados escolares e estágios. **Vai dar merda** se apagares por engano! + Tens a certeza que queres eliminar o registo de {alunoParaEliminar?.nome}? + + + Isto apagará também os estágios e presenças! setShowDeleteModal(false)} > Cancelar @@ -255,7 +312,7 @@ const ListaAlunosProfessor = memo(() => { {isDeleting ? ( ) : ( - Eliminar Tudo + Sim, Apagar )} @@ -263,14 +320,6 @@ const ListaAlunosProfessor = memo(() => { - {/* BOTÃO FLUTUANTE */} - router.push('/Professor/Alunos/CriarAluno')} - > - - Novo Aluno - ); @@ -278,34 +327,47 @@ const ListaAlunosProfessor = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900' }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' }, + // TOAST STYLES + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, 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 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - searchSection: { paddingHorizontal: 24, marginBottom: 10 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, - searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, - listPadding: { paddingHorizontal: 24, paddingTop: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, + + searchSection: { paddingHorizontal: 20, marginBottom: 15 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + searchInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + + listPadding: { paddingHorizontal: 20, paddingTop: 10 }, + + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase' }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1 }, - avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, fontWeight: '900' }, - alunoInfo: { flex: 1, marginLeft: 15 }, - alunoNome: { fontSize: 16, fontWeight: '800' }, - idText: { fontSize: 13, fontWeight: '600' }, - fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5 }, - fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10 }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', alignItems: 'center', padding: 30 }, - modalCard: { width: '100%', borderRadius: 32, padding: 24, alignItems: 'center' }, + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, + avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 20, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 }, + idText: { fontSize: 13, fontWeight: '700' }, + trashBtn: { padding: 8 }, + + emptyContainer: { marginTop: 100, alignItems: 'center' }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, + emptyText: { fontSize: 15, fontWeight: '700' }, + + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', padding: 24 }, + modalCard: { width: '100%', borderRadius: 32, padding: 24, alignItems: 'center', elevation: 10 }, warningIconBox: { width: 70, height: 70, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - modalTitle: { fontSize: 20, fontWeight: '900', marginBottom: 10, textAlign: 'center' }, - modalDesc: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 25 }, + modalTitle: { fontSize: 22, fontWeight: '900', marginBottom: 8, textAlign: 'center', letterSpacing: -0.5 }, + modalDesc: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 10 }, + modalWarning: { fontSize: 12, fontWeight: '800', textAlign: 'center', textTransform: 'uppercase', marginBottom: 25 }, modalButtons: { flexDirection: 'row', gap: 12, width: '100%' }, - btnModal: { flex: 1, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - btnText: { fontSize: 16, fontWeight: '800' } + btnModal: { flex: 1, height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + btnText: { fontSize: 15, fontWeight: '800', letterSpacing: 0.5 } }); export default ListaAlunosProfessor; \ No newline at end of file diff --git a/app/Professor/Alunos/Presencas.tsx b/app/Professor/Alunos/Presencas.tsx index 8cd9126..7e2e720 100644 --- a/app/Professor/Alunos/Presencas.tsx +++ b/app/Professor/Alunos/Presencas.tsx @@ -39,14 +39,15 @@ const Presencas = memo(() => { const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Atualizado para o 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.12)' : '#F0F9FA', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', }), [isDarkMode]); const fetchAlunos = async () => { @@ -105,14 +106,13 @@ const Presencas = memo(() => { return ( - + - + - {/* HEADER EPVC */} router.back()} > @@ -120,24 +120,23 @@ const Presencas = memo(() => { Presenças - Controlo Diário + Estágios+ - {/* SEARCH BAR */} - + { item.nome} - contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 40 }]} + showsVerticalScrollIndicator={false} refreshControl={} renderItem={({ item }) => ( - - {/* SEPARADOR DE TURMA */} + + {item.nome} @@ -165,7 +165,7 @@ const Presencas = memo(() => { {item.alunos.map((aluno) => ( router.push({ @@ -181,15 +181,15 @@ const Presencas = memo(() => { - {aluno.nome} + {aluno.nome} - - Abrir Calendário + + Nº {aluno.n_escola} • Abrir Registo - - + + ))} @@ -197,8 +197,10 @@ const Presencas = memo(() => { )} ListEmptyComponent={() => ( - - Sem resultados. + + + + Nenhum aluno encontrado. )} /> @@ -210,27 +212,33 @@ const Presencas = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - searchSection: { paddingHorizontal: 24, marginBottom: 10 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, - searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, - listPadding: { paddingHorizontal: 24, paddingTop: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, + + searchSection: { paddingHorizontal: 20, marginBottom: 15 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + searchInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + + listPadding: { paddingHorizontal: 20, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, avatarText: { fontSize: 20, fontWeight: '900' }, - alunoInfo: { flex: 1, marginLeft: 15 }, - alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, - idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, - idText: { fontSize: 13, fontWeight: '600' }, - statusBadge: { width: 34, height: 34, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5 }, + idText: { fontSize: 12, fontWeight: '700' }, + statusBadge: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + emptyContainer: { marginTop: 100, alignItems: 'center' }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, + emptyText: { fontSize: 15, fontWeight: '700' }, }); -export default Presencas; \ No newline at end of file +export default Presencas; diff --git a/app/Professor/Alunos/Sumarios.tsx b/app/Professor/Alunos/Sumarios.tsx index cb08b1f..71dff23 100644 --- a/app/Professor/Alunos/Sumarios.tsx +++ b/app/Professor/Alunos/Sumarios.tsx @@ -1,9 +1,10 @@ // app/(Professor)/SumariosAlunos.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, + Animated, FlatList, Modal, RefreshControl, @@ -44,26 +45,43 @@ const SumariosAlunos = memo(() => { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - // Estados do Modal + // Estados do Modal e Avisos const [modalVisible, setModalVisible] = useState(false); const [alunoSelecionado, setAlunoSelecionado] = useState(null); const [sumarios, setSumarios] = useState([]); const [loadingSumarios, setLoadingSumarios] = useState(false); + 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 azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', azul: azulEPVC, laranja: laranjaEPVC, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + vermelho: erroCor, + verde: sucessoCor, }), [isDarkMode]); + 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]); + const fetchAlunos = async () => { try { setLoading(true); @@ -93,6 +111,7 @@ const SumariosAlunos = memo(() => { ); } catch (err) { console.error(err); + showToast("Erro ao carregar as turmas.", "error"); } finally { setLoading(false); setRefreshing(false); @@ -131,8 +150,10 @@ const SumariosAlunos = memo(() => { if (error) throw error; setSumarios(data || []); + } catch (err) { console.error(err); + showToast("Erro ao carregar o caderno.", "error"); } finally { setLoadingSumarios(false); } @@ -140,7 +161,6 @@ const SumariosAlunos = memo(() => { useEffect(() => { fetchAlunos(); }, []); - // Abre automaticamente se vier de outra página com params useEffect(() => { if (params.alunoId && typeof params.alunoId === 'string' && !modalVisible) { abrirSumarios({ id: params.alunoId, nome: (params.nome as string) || 'Aluno', n_escola: '', turma: '' }); @@ -165,13 +185,30 @@ const SumariosAlunos = memo(() => { return ( - - + + + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + - {/* HEADER */} + {/* HEADER MODERNO */} router.back()} > @@ -179,24 +216,24 @@ const SumariosAlunos = memo(() => { Sumários - Caderno de Registos + Estágios+ - {/* SEARCH */} + {/* SEARCH SECTION MODERNA */} - + { data={filteredTurmas} keyExtractor={item => item.nome} contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + showsVerticalScrollIndicator={false} refreshControl={} renderItem={({ item }) => ( - + + {item.nome} @@ -223,7 +262,7 @@ const SumariosAlunos = memo(() => { {item.alunos.map((aluno) => ( abrirSumarios(aluno)} > @@ -234,20 +273,28 @@ const SumariosAlunos = memo(() => { - {aluno.nome} + {aluno.nome} - + Nº {aluno.n_escola} • Ver Caderno - - + + ))} )} + ListEmptyComponent={() => ( + + + + + Nenhum aluno encontrado. + + )} /> )} @@ -255,10 +302,11 @@ const SumariosAlunos = memo(() => { setModalVisible(false)}> - - - Caderno de Sumários - {alunoSelecionado?.nome} + + + + Caderno Diário + {alunoSelecionado?.nome} setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> @@ -266,20 +314,23 @@ const SumariosAlunos = memo(() => { {loadingSumarios ? ( - + ) : ( - + {sumarios.length === 0 ? ( - - - Sem sumários registados. + + + + + Sem sumários registados. + Os diários de bordo irão aparecer aqui. ) : ( sumarios.map((s) => ( - + {new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })} @@ -303,39 +354,52 @@ const SumariosAlunos = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + // TOAST STYLES + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, 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 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - searchSection: { paddingHorizontal: 24, marginBottom: 10 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, - searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, - listPadding: { paddingHorizontal: 24, paddingTop: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18, marginTop: 10 }, + + searchSection: { paddingHorizontal: 20, marginBottom: 15 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + searchInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + + listPadding: { paddingHorizontal: 20, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, marginTop: 10 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, - avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, fontWeight: '900' }, - alunoInfo: { flex: 1, marginLeft: 15 }, - alunoNome: { fontSize: 16, fontWeight: '800' }, - idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, - idText: { fontSize: 12, fontWeight: '600' }, - statusBadge: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, + avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 20, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5 }, + idText: { fontSize: 12, fontWeight: '700' }, + statusBadge: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, - modalContent: { height: '85%', borderTopLeftRadius: 40, borderTopRightRadius: 40 }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 24 }, - modalTitle: { fontSize: 20, fontWeight: '900' }, - modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase' }, - closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - sumarioCard: { padding: 20, borderRadius: 24, marginBottom: 16, borderWidth: 1 }, - sumarioTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, - dateTag: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 10 }, - dateTagText: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase' }, - dotLine: { flex: 1, height: 1, marginLeft: 12, opacity: 0.3 }, - sumarioTexto: { fontSize: 15, lineHeight: 22, fontWeight: '600' }, - emptyContainer: { marginTop: 80, alignItems: 'center' }, + modalContent: { height: '88%', borderTopLeftRadius: 32, borderTopRightRadius: 32, overflow: 'hidden' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, paddingBottom: 15, borderBottomWidth: 1 }, + modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 4 }, + closeBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + + sumarioCard: { padding: 20, borderRadius: 24, marginBottom: 16, borderWidth: 1, elevation: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 5 }, + sumarioTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 16 }, + dateTag: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12 }, + dateTagText: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase' }, + dotLine: { flex: 1, height: 1.5, marginLeft: 12, opacity: 0.4 }, + sumarioTexto: { fontSize: 15, lineHeight: 24, fontWeight: '600' }, + + emptyContainer: { marginTop: 100, alignItems: 'center' }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, + emptyText: { fontSize: 15, fontWeight: '700' }, + emptySubText: { fontSize: 13, fontWeight: '600', marginTop: 4 } }); export default SumariosAlunos; \ No newline at end of file diff --git a/app/Professor/Empresas/DetalhesEmpresa.tsx b/app/Professor/Empresas/DetalhesEmpresa.tsx index cf29117..d94661d 100644 --- a/app/Professor/Empresas/DetalhesEmpresa.tsx +++ b/app/Professor/Empresas/DetalhesEmpresa.tsx @@ -1,9 +1,10 @@ // app/Professor/Empresas/DetalhesEmpresa.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, + Animated, KeyboardAvoidingView, Modal, Platform, @@ -28,6 +29,16 @@ export interface Empresa { curso: string; } +// Interface para os detalhes do estágio no Modal +interface DetalhesEstagio { + id: string; + data_inicio: string; + data_fim: string; + horas_diarias: string; + horas_totais: number; + horarios: { periodo: string; hora_inicio: string; hora_fim: string }[]; +} + const DetalhesEmpresa = memo(() => { const { isDarkMode } = useTheme(); const router = useRouter(); @@ -36,6 +47,8 @@ const DetalhesEmpresa = memo(() => { const azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; const empresaOriginal: Empresa = useMemo(() => { if (!params.empresa) return {} as Empresa; @@ -52,23 +65,45 @@ const DetalhesEmpresa = memo(() => { const [loadingAlunos, setLoadingAlunos] = useState(true); const [editando, setEditando] = useState(false); const [loading, setLoading] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + // Estados para o Modal de Detalhes do Estágio + const [estagioModalVisible, setEstagioModalVisible] = useState(false); + const [alunoSelecionadoParaEstagio, setAlunoSelecionadoParaEstagio] = useState<{ id: string; nome: string } | null>(null); + const [detalhesEstagio, setDetalhesEstagio] = useState(null); + const [loadingEstagio, setLoadingEstagio] = useState(false); + + // ESTADOS DO 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 cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', azul: azulEPVC, laranja: laranjaEPVC, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', - vermelho: '#EF4444', - vermelhoSuave: '#FFF5F5', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - overlay: 'rgba(26, 54, 93, 0.8)', + vermelho: erroCor, + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', + verde: sucessoCor, + verdeSuave: isDarkMode ? 'rgba(16, 185, 129, 0.15)' : '#DCFCE7', + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + overlay: 'rgba(0, 0, 0, 0.6)', }), [isDarkMode]); + 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]); + useEffect(() => { if (empresaLocal.id) carregarAlunos(); }, [empresaLocal.id]); @@ -89,6 +124,46 @@ const DetalhesEmpresa = memo(() => { } } + // Função para carregar os detalhes do estágio quando clica num aluno + const abrirDetalhesEstagio = async (aluno: { id: string; nome: string }) => { + setAlunoSelecionadoParaEstagio(aluno); + setEstagioModalVisible(true); + setLoadingEstagio(true); + setDetalhesEstagio(null); + + try { + // Vai buscar o estágio ativo deste aluno nesta empresa + const { data: estagioData, error: estagioError } = await supabase + .from('estagios') + .select('id, data_inicio, data_fim, horas_diarias, horas_totais') + .eq('aluno_id', aluno.id) + .eq('empresa_id', empresaLocal.id) + .order('data_inicio', { ascending: false }) + .limit(1) + .single(); + + if (estagioError) throw estagioError; + + // Vai buscar os horários + const { data: horariosData } = await supabase + .from('horarios_estagio') + .select('periodo, hora_inicio, hora_fim') + .eq('estagio_id', estagioData.id); + + setDetalhesEstagio({ + ...estagioData, + horarios: horariosData || [] + }); + + } catch (error) { + console.error(error); + showToast('Não foi possível carregar os detalhes do estágio.', 'error'); + setEstagioModalVisible(false); + } finally { + setLoadingEstagio(false); + } + }; + const handleSave = async () => { try { setLoading(true); @@ -103,10 +178,9 @@ const DetalhesEmpresa = memo(() => { if (error) throw error; setEditando(false); - setShowSuccess(true); - setTimeout(() => setShowSuccess(false), 3000); - } catch (error) { - console.error(error); + showToast('Dados atualizados com sucesso!', 'success'); + } catch (error: any) { + showToast(error.message || 'Não foi possível guardar as alterações.', 'error'); } finally { setLoading(false); } @@ -121,69 +195,201 @@ const DetalhesEmpresa = memo(() => { router.back(); } catch (e) { setShowDeleteModal(false); - alert('Não é possível apagar empresas com estágios ativos.'); + showToast('Não é possível apagar empresas com estágios ativos.', 'error'); } finally { setLoading(false); } }; + // Helper para formatar a data + const formatarData = (dataStr: string) => { + if (!dataStr) return 'Não definida'; + const partes = dataStr.split('-'); + if (partes.length === 3) return `${partes[2]}-${partes[1]}-${partes[0]}`; + return dataStr; + }; + + // Helper para formatar a hora + const formatarHora = (horaStr: string) => { + if (!horaStr) return ''; + return horaStr.slice(0, 5); // "09:00:00" -> "09:00" + }; + return ( - + - {showSuccess && ( - - - Dados atualizados! - - )} + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + {/* DELETE MODAL */} - - + + - Eliminar Entidade? - + Eliminar Entidade? + Esta ação é irreversível. A empresa {empresaLocal.nome} será removida do sistema. - + setShowDeleteModal(false)} > - Cancelar + CANCELAR - {loading ? : Confirmar} + {loading ? : ELIMINAR} - + {/* MODAL DE DETALHES DO ESTÁGIO (NOVO) */} + setEstagioModalVisible(false)}> + + + + + + + Plano de Estágio + {alunoSelecionadoParaEstagio?.nome} + + setEstagioModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.fundo }]}> + + + + + {loadingEstagio ? ( + + ) : detalhesEstagio ? ( + + + {/* DATAS E HORAS */} + + + + INÍCIO + {formatarData(detalhesEstagio.data_inicio)} + + + FIM (PREV.) + {formatarData(detalhesEstagio.data_fim)} + + + + + + + + HORAS/DIA + + {detalhesEstagio.horas_diarias || 'N/A'} + + + + HORAS TOTAIS + + {detalhesEstagio.horas_totais || '0'}h + + + + + + {/* HORÁRIOS */} + + Horário de Trabalho + + {detalhesEstagio.horarios.length > 0 ? ( + detalhesEstagio.horarios.map((horario, index) => ( + + + {horario.periodo} + + + {formatarHora(horario.hora_inicio)} + + - + + {formatarHora(horario.hora_fim)} + + + + )) + ) : ( + Horários não definidos. + )} + + + {/* BOTÃO PARA PRESENÇAS */} + { + setEstagioModalVisible(false); + router.push({ + pathname: '/Professor/HistoricoPresencas', + params: { alunoId: alunoSelecionadoParaEstagio?.id, nome: alunoSelecionadoParaEstagio?.nome } + }); + }} + > + + Ver Histórico de Presenças + + + + ) : ( + Não foi possível carregar os dados. + )} + + + + + + + {/* HEADER MODERNO */} router.back()} > - + Ficha Técnica - ID: #{empresaLocal.id?.slice(0, 5)} + ID: #{empresaLocal.id?.toString().slice(0, 5)} { if(editando) setEmpresaLocal({...empresaOriginal}); setEditando(!editando); @@ -222,17 +428,25 @@ const DetalhesEmpresa = memo(() => { {loadingAlunos ? '-' : alunos.length} - {loadingAlunos ? : alunos.length > 0 ? ( + {loadingAlunos ? : alunos.length > 0 ? ( alunos.map((aluno, i) => ( - + abrirDetalhesEstagio(aluno)} + > - {aluno.nome} - - + + {aluno.nome} + Ver plano de estágio + + + )) - ) : Nenhum aluno vinculado a esta entidade.} + ) : Nenhum aluno vinculado a esta entidade no momento.} @@ -261,12 +475,19 @@ const DetalhesEmpresa = memo(() => { const ModernField = ({ label, value, editable, cores, icon, ...props }: any) => ( - + {label} {editable ? ( const styles = StyleSheet.create({ safe: { flex: 1 }, - toast: { position: 'absolute', left: 25, right: 25, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, gap: 12, elevation: 8 }, - toastText: { color: '#fff', fontSize: 14, fontWeight: '900' }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + // 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 }, headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - scroll: { paddingHorizontal: 24, paddingTop: 10 }, - card: { padding: 22, borderRadius: 32, borderWidth: 1, marginBottom: 18, elevation: 2, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, gap: 10 }, + + scroll: { paddingHorizontal: 20, paddingTop: 10 }, + + card: { padding: 24, borderRadius: 28, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 22, gap: 10 }, sideLine: { width: 4, height: 18, borderRadius: 2 }, - sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + fieldContainer: { marginBottom: 18 }, - labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 6, marginLeft: 4 }, - fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, - input: { fontSize: 15, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 16, borderWidth: 1.5 }, + labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 8, marginLeft: 4 }, + fieldLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + input: { fontSize: 15, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 14, borderRadius: 16, borderWidth: 1.5 }, readOnly: { paddingHorizontal: 16, paddingVertical: 14, borderRadius: 16, borderWidth: 1 }, - readOnlyTxt: { fontSize: 15, fontWeight: '700' }, - countBadge: { width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' }, + readOnlyTxt: { fontSize: 15, fontWeight: '600' }, + + countBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginLeft: 'auto' }, countBadgeTxt: { color: '#fff', fontSize: 11, fontWeight: '900' }, + alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14 }, - miniAvatar: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 12 }, - alunoName: { fontSize: 15, fontWeight: '700', flex: 1 }, - empty: { textAlign: 'center', fontSize: 13, fontWeight: '600', paddingVertical: 10, fontStyle: 'italic' }, - footerActions: { marginTop: 10 }, - btnSave: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', elevation: 5, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 }, + miniAvatar: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginRight: 14 }, + alunoName: { fontSize: 15, fontWeight: '800', letterSpacing: -0.3 }, + empty: { textAlign: 'center', fontSize: 14, fontWeight: '600', paddingVertical: 15, opacity: 0.7 }, + + footerActions: { marginTop: 10, marginBottom: 20 }, + btnSave: { height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 8 }, btnRow: { flexDirection: 'row', alignItems: 'center', gap: 10 }, - btnSaveTxt: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }, - btnDel: { height: 56, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 1.5, borderStyle: 'dashed' }, + btnSaveTxt: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + btnDel: { height: 56, borderRadius: 18, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 1.5, borderStyle: 'dashed' }, btnDelTxt: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, - modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 25 }, - modalContent: { width: '100%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1 }, - iconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - modalTitle: { fontSize: 24, fontWeight: '900', marginBottom: 12 }, - modalSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 }, - modalButtons: { flexDirection: 'row', gap: 15 }, - modalBtn: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - modalBtnTxt: { fontSize: 15, fontWeight: '900' } + + // DELETE MODAL STYLES + modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.6)', paddingHorizontal: 20 }, + deleteCard: { width: '85%', borderRadius: 36, padding: 30, alignItems: 'center', borderWidth: 1, elevation: 10, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 10 }, + deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + deleteTitle: { fontSize: 22, fontWeight: '900', textAlign: 'center', marginBottom: 12, letterSpacing: -0.5 }, + deleteSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, fontWeight: '500', marginBottom: 30 }, + deleteFooter: { flexDirection: 'row', gap: 15, width: '100%' }, + deleteBtnCancel: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + deleteBtnText: { fontSize: 14, fontWeight: '900' }, + + // ESTAGIO MODAL STYLES (NOVO) + estagioModalContent: { width: '100%', borderTopLeftRadius: 36, borderTopRightRadius: 36, padding: 25, maxHeight: '85%' }, + modalIndicator: { width: 45, height: 5, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, alignSelf: 'center', marginBottom: 20 }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 }, + modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + modalSubtitle: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', marginTop: 4, letterSpacing: 0.5 }, + closeBtn: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + + infoBox: { borderRadius: 24, borderWidth: 1, padding: 20 }, + infoTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 0.5 }, + infoRow: { flexDirection: 'row', justifyContent: 'space-between' }, + infoCol: { flex: 1 }, + infoLabel: { fontSize: 9, fontWeight: '900', textTransform: 'uppercase', marginBottom: 6, letterSpacing: 0.5 }, + infoValue: { fontSize: 15, fontWeight: '800' }, + divider: { height: 1, opacity: 0.5, marginVertical: 15 }, + + badge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start' }, + badgeTxt: { fontSize: 13, fontWeight: '900' }, + + horarioRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, + horarioPeriodo: { fontSize: 13, fontWeight: '800', width: 60, marginLeft: 8 }, + horarioTags: { flexDirection: 'row', alignItems: 'center', flex: 1, justifyContent: 'flex-end' }, + horarioTime: { fontSize: 14, fontWeight: '700', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, overflow: 'hidden' }, + + btnActionSecondary: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: 56, borderRadius: 18, borderWidth: 1.5, borderStyle: 'dashed', gap: 8 }, + btnActionSecondaryTxt: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 } }); export default DetalhesEmpresa; \ No newline at end of file diff --git a/app/Professor/Empresas/ListaEmpresas.tsx b/app/Professor/Empresas/ListaEmpresas.tsx index ef1ba30..3d391bd 100644 --- a/app/Professor/Empresas/ListaEmpresas.tsx +++ b/app/Professor/Empresas/ListaEmpresas.tsx @@ -1,15 +1,11 @@ // app/Professor/Empresas/ListaEmpresasProfessor.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, - Alert, - KeyboardAvoidingView, - Modal, - Platform, + Animated, RefreshControl, - ScrollView, SectionList, StatusBar, StyleSheet, @@ -40,25 +36,42 @@ const ListaEmpresasProfessor = memo(() => { const [empresas, setEmpresas] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const [modalVisible, setModalVisible] = useState(false); - const [form, setForm] = useState({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' }); + // ESTADOS DO TOAST + 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; // Cores EPVC const azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', azul: azulEPVC, laranja: laranjaEPVC, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + vermelho: erroCor, + verde: sucessoCor, + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + overlay: 'rgba(0, 0, 0, 0.6)', }), [isDarkMode]); + 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]); + const fetchEmpresas = async () => { try { setLoading(true); @@ -70,7 +83,7 @@ const ListaEmpresasProfessor = memo(() => { if (error) throw error; setEmpresas(data || []); } catch (error: any) { - Alert.alert('Erro', 'Não foi possível carregar as empresas.'); + showToast('Não foi possível carregar as empresas.', 'error'); } finally { setLoading(false); setRefreshing(false); @@ -103,47 +116,32 @@ const ListaEmpresasProfessor = memo(() => { })).sort((a, b) => a.title.localeCompare(b.title)); }, [search, empresas]); - const criarEmpresa = async () => { - if (!form.nome || !form.morada || !form.tutorNome || !form.tutorTelefone || !form.curso) { - Alert.alert('Atenção', 'Preencha todos os campos.'); - return; - } - - try { - setLoading(true); - const { data, error } = await supabase - .from('empresas') - .insert([{ - nome: form.nome.trim(), - morada: form.morada.trim(), - tutor_nome: form.tutorNome.trim(), - tutor_telefone: form.tutorTelefone.trim(), - curso: form.curso.trim().toUpperCase(), - }]) - .select(); - - if (error) throw error; - setEmpresas(prev => [...prev, data![0]]); - setModalVisible(false); - setForm({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' }); - Alert.alert('Sucesso', 'Empresa registada com sucesso!'); - } catch (error: any) { - Alert.alert('Erro', error.message); - } finally { - setLoading(false); - } - }; - return ( - + - + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + + + - {/* HEADER EPVC */} + {/* HEADER MODERNO */} router.back()} > @@ -155,19 +153,12 @@ const ListaEmpresasProfessor = memo(() => { {empresas.length} parcerias ativas - - setModalVisible(true)} - > - - {/* PESQUISA */} - + { sections={secoesAgrupadas} keyExtractor={item => item.id.toString()} stickySectionHeadersEnabled={false} + showsVerticalScrollIndicator={false} contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} refreshControl={} renderSectionHeader={({ section: { title } }) => ( @@ -198,119 +190,76 @@ const ListaEmpresasProfessor = memo(() => { )} renderItem={({ item }) => ( router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', params: { empresa: JSON.stringify(item) } })} > - + - {item.nome} + {item.nome} - + {item.morada} - - + + )} ListEmptyComponent={() => ( - - Nenhuma entidade encontrada. + + + + Nenhuma entidade encontrada. )} /> )} - - {/* MODAL NOVO REGISTO */} - - - - - - Registar Parceiro - Nova entidade de estágio - - setModalVisible(false)} style={styles.closeBtn}> - - - - - - setForm({...form, nome:v})} cores={cores} /> - setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI, MULT, etc." /> - setForm({...form, morada:v})} cores={cores} /> - - - - setForm({...form, tutorNome:v})} cores={cores} /> - setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} /> - - - Confirmar Registo - - - - - ); }); -const ModernInput = ({ label, icon, cores, ...props }: any) => ( - - {label} - - - - - -); - const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15 }, + // TOAST STYLES + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, 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', paddingHorizontal: 20, paddingVertical: 15 }, backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - headerTitle: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8 }, - headerSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, - addBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 8 }, - searchSection: { paddingHorizontal: 24, marginVertical: 15 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' }, - loadingCenter: { marginTop: 60, alignItems: 'center' }, - emptyContainer: { marginTop: 100, alignItems: 'center' }, - listPadding: { paddingHorizontal: 24 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 30, marginBottom: 15 }, + headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 }, + + searchSection: { paddingHorizontal: 20, marginBottom: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + searchInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + + loadingCenter: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + + listPadding: { paddingHorizontal: 20 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 20, marginBottom: 15 }, sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, - sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }, - sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, - empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 28, marginBottom: 14, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 12 }, - empresaIcon: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, - empresaInfo: { flex: 1, marginLeft: 15 }, - empresaNome: { fontSize: 17, fontWeight: '800', letterSpacing: -0.3 }, - tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 4 }, - tutorText: { fontSize: 12, fontWeight: '600' }, - arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(26, 54, 93, 0.8)', justifyContent: 'flex-end' }, - modalContent: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 28, maxHeight: '90%' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 30 }, - modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, - modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 }, - closeBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' }, - divider: { height: 1, marginVertical: 20, opacity: 0.5 }, - inputWrapper: { marginBottom: 20 }, - inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, - inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 58, borderRadius: 18, borderWidth: 1.5 }, - textInput: { flex: 1, fontSize: 15, fontWeight: '700' }, - saveBtn: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', marginTop: 25, elevation: 6, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 }, - saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2 } + sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 }, + + empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, + empresaIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + empresaInfo: { flex: 1, marginLeft: 16, paddingRight: 10 }, + empresaNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 }, + tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + tutorText: { fontSize: 13, fontWeight: '600' }, + arrowCircle: { width: 34, height: 34, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + + emptyContainer: { marginTop: 100, alignItems: 'center' }, + emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, + emptyText: { fontSize: 15, fontWeight: '700' } }); export default ListaEmpresasProfessor; \ No newline at end of file diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index e7ce7da..d28701b 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -47,20 +47,25 @@ export default function PerfilProfessor() { Animated.delay(3000), Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) ]).start(() => setAlertConfig(null)); - }, []); + }, [alertOpacity]); + + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', - azul: '#2390a6', - laranja: '#E38E00', - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', - vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FFF5F5', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + 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', + laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', vermelho: '#EF4444', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#38A169', + verde: '#10B981', + inputFundo: isDarkMode ? '#252525' : '#F8FAFC', }), [isDarkMode]); useEffect(() => { carregarPerfil(); }, []); @@ -69,7 +74,6 @@ export default function PerfilProfessor() { try { setLoading(true); - // 1. Obter a sessão atual de forma limpa const { data: { session }, error: sessionError } = await supabase.auth.getSession(); if (sessionError || !session) { @@ -77,18 +81,15 @@ export default function PerfilProfessor() { return; } - // 2. Buscar o perfil filtrando pelo ID da sessão e garantindo que o tipo é PROFESSOR - // Isso impede que, se a sessão mudar para aluno, os dados apareçam aqui const { data, error } = await supabase .from('profiles') .select('*') .eq('id', session.user.id) - .eq('tipo', 'professor') // Filtro de segurança + .eq('tipo', 'professor') .single(); if (error || !data) { - // Se for um aluno a tentar aceder a esta página de professor, expulsamos - showAlert('Acesso negado ou perfil não encontrado.', 'error'); + showAlert('Perfil não encontrado.', 'error'); await supabase.auth.signOut(); router.replace('/'); return; @@ -105,6 +106,19 @@ export default function PerfilProfessor() { const guardarPerfil = async () => { if (!perfil) return; + + // 🟢 VALIDAÇÃO: Se algum campo estiver vazio ou for apenas espaços, dispara erro + if ( + !perfil.nome?.trim() || + !perfil.telefone?.trim() || + !perfil.residencia?.trim() || + !perfil.n_escola?.trim() || + !perfil.curso?.trim() + ) { + showAlert('Todos os campos são de preenchimento obrigatório.', 'error'); + return; // Corta a função aqui para não gravar na base de dados + } + try { const { error } = await supabase .from('profiles') @@ -119,7 +133,7 @@ export default function PerfilProfessor() { if (error) throw error; setEditando(false); - showAlert('Perfil atualizado!', 'success'); + showAlert('Perfil atualizado com sucesso!', 'success'); } catch (error: any) { showAlert('Erro ao gravar dados.', 'error'); } @@ -142,107 +156,169 @@ export default function PerfilProfessor() { - {alertConfig && ( + {alertConfig ? ( - - {alertConfig.msg} + + {alertConfig.msg} - )} + ) : null} - - router.back()}> - + {/* HEADER */} + + router.back()}> + - O Meu Perfil + + O Meu Perfil + Estágios+ + editando ? guardarPerfil() : setEditando(true)} > - + + {/* HEADER DO PERFIL (AVATAR) */} - + {perfil?.nome?.charAt(0).toUpperCase()} {perfil?.nome} - - PROFESSOR • {perfil?.curso} + + + Professor • {perfil?.curso || 'EPVC'} - - setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} /> + + + {/* CARTÃO: DADOS PESSOAIS */} + + + + setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} /> + + + + + + setPerfil(prev => prev ? { ...prev, telefone: v } : null)} cores={cores} /> + + - setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} /> + setPerfil(prev => prev ? { ...prev, residencia: v } : null)} cores={cores} /> + - - - - - setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} /> + {/* CARTÃO: DADOS PROFISSIONAIS */} + + + + + + setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} /> - setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} /> + setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} /> + + + {/* BOTÕES DE AÇÃO (SEGURANÇA E SAIR) */} + + router.push('/Professor/redefenirsenha2')} + activeOpacity={0.7} + > + + + + Alterar palavra-passe + + + + + + + + Terminar sessão + + + + {/* BOTÃO DE CANCELAR EDIÇÃO */} + {editando ? ( + { setEditando(false); carregarPerfil(); }}> + Cancelar e Reverter Alterações + + ) : null} + - - - router.push('/Professor/redefenirsenha2')}> - - - - Segurança da Conta - - - - - - - - Sair do Sistema - - - - {editando && ( - { setEditando(false); carregarPerfil(); }}> - Reverter Alterações - - )} - ); } +// COMPONENTE HEADER DE SECÇÃO +const SectionHeader = ({ icon, title, cores }: any) => ( + + + + + {title} + +); + +// COMPONENTE DE INPUT MODERNO const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( {label} - - - + + + ); @@ -250,30 +326,48 @@ const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( const styles = StyleSheet.create({ safe: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 }, - alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, fontSize: 13 }, - topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, - backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - editBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1, elevation: 2 }, - topTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, - scrollContent: { paddingHorizontal: 24, paddingBottom: 50 }, - profileHeader: { alignItems: 'center', marginVertical: 35 }, - avatarBorder: { padding: 4, borderRadius: 100, borderWidth: 2, position: 'relative' }, - avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center' }, + + // TOAST + toastContainer: { position: 'absolute', left: 20, right: 20, padding: 16, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, + toastText: { color: '#fff', fontWeight: '800', marginLeft: 12, fontSize: 14, flex: 1 }, + + // HEADER SUPERIOR + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginTop: 2 }, + + scrollContent: { paddingHorizontal: 20, paddingBottom: 50 }, + + // PERFIL HIGHLIGHT + profileHeader: { alignItems: 'center', marginVertical: 25 }, + avatarBorder: { padding: 6, borderRadius: 100, borderWidth: 2 }, + avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', elevation: 4, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 }, avatarLetter: { color: '#fff', fontSize: 36, fontWeight: '900' }, userName: { fontSize: 24, fontWeight: '900', marginTop: 15, letterSpacing: -0.5 }, - roleBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 }, - userRole: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, - card: { borderRadius: 28, padding: 24, marginBottom: 20, borderWidth: 1 }, - inputWrapper: { marginBottom: 18 }, - inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, - inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + roleBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, marginTop: 8 }, + userRole: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }, + + cardsContainer: { gap: 20 }, + + // CARTÕES DE SECÇÃO + sectionCard: { borderRadius: 24, padding: 20, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, + iconCircle: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 10 }, + sectionTitleTxt: { fontSize: 16, fontWeight: '900', letterSpacing: -0.2 }, + + // INPUTS + inputWrapper: { marginBottom: 16 }, + inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 52, borderRadius: 16 }, textInput: { flex: 1, fontSize: 15, fontWeight: '700' }, - row: { flexDirection: 'row' }, - actionsContainer: { gap: 12 }, - menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1 }, + + // ACÇÕES E MENU + actionsContainer: { gap: 12, marginTop: 10 }, + menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1, elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.02, shadowRadius: 5 }, menuIcon: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, menuText: { flex: 1, marginLeft: 15, fontSize: 15, fontWeight: '800' }, - cancelBtn: { marginTop: 25, alignItems: 'center' }, + + cancelBtn: { marginTop: 20, alignItems: 'center', paddingVertical: 10 }, cancelText: { fontSize: 14, fontWeight: '800', textDecorationLine: 'underline' } }); \ No newline at end of file diff --git a/app/Professor/ProfessorHome.tsx b/app/Professor/ProfessorHome.tsx index 412550b..a7a4813 100644 --- a/app/Professor/ProfessorHome.tsx +++ b/app/Professor/ProfessorHome.tsx @@ -1,4 +1,4 @@ -// app/Professor/ProfessorMenu.tsx +// app/Professor/ProfessorHome.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useEffect, useMemo, useState } from 'react'; @@ -18,25 +18,26 @@ import { useTheme } from '../../themecontext'; const { width } = Dimensions.get('window'); -export default function ProfessorMenu() { +export default function ProfessorHome() { const router = useRouter(); const { isDarkMode } = useTheme(); const insets = useSafeAreaInsets(); const [nome, setNome] = useState(''); const [loading, setLoading] = useState(true); - // Paleta EPVC Extraída da Imagem + // Paleta EPVC const azulEPVC = '#2390a6'; const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Fundo branco limpo como o login - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', // Azul escuro para contraste - textoSecundario: isDarkMode ? '#94A3B8' : '#718096', + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', azul: azulEPVC, laranja: laranjaEPVC, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', + laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); @@ -69,105 +70,173 @@ export default function ProfessorMenu() { - {/* HEADER EPVC STYLE */} + {/* Cabeçalho */} - - Olá, Professor + + + + Escola Profissional de Vila do Conde + {loading ? ( - + ) : ( - {nome || 'Docente'} + + Olá, {nome ? nome.split(' ')[0] : 'Professor'} + )} + Controlo de Estágios Escolares + router.push('/Professor/PerfilProf')} - style={[styles.avatarMini, { backgroundColor: cores.azul }]} + style={[styles.avatarMini, { backgroundColor: cores.azulSuave, borderColor: cores.azul }]} > - {nome?.charAt(0).toUpperCase() || 'P'} + {nome?.charAt(0).toUpperCase() || 'P'} - - - - Escola Profissional de Vila do Conde - - {/* GRID DE MENU */} - - router.push('/Professor/Alunos/Sumarios')} cores={cores} /> - router.push('/Professor/Alunos/Presencas')} cores={cores} /> - router.push('/Professor/Alunos/Faltas')} cores={cores} /> - router.push('/Professor/Alunos/ListaAlunos')} cores={cores} /> - router.push('/Professor/Alunos/Estagios')} cores={cores} /> - router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} /> - router.push('/Professor/defenicoes2')} cores={cores} /> - router.push('/Professor/PerfilProf')} cores={cores} /> + {/* Criar Registo */} + router.push('/Professor/Alunos/CriarRegisto')} + > + + + + Novo Registo + Adicionar Empresa, Aluno ou Professor + + + + + + + + {/* SECÇÃO: Controlo Diário */} + + Controlo diário dos alunos + + router.push('/Professor/Alunos/Presencas')} cores={cores} corDestaque={cores.azul} /> + router.push('/Professor/Alunos/Sumarios')} cores={cores} corDestaque={cores.laranja} /> + router.push('/Professor/Alunos/Faltas')} cores={cores} corDestaque="#EF4444" /> + + + + {/* SECÇÃO: Empresas */} + + Entidades + + router.push('/Professor/Alunos/ListaAlunos')} cores={cores} corDestaque={cores.azul} fullWidth /> + router.push('/Professor/Alunos/Estagios')} cores={cores} corDestaque={cores.laranja} /> + router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} corDestaque={cores.azul} /> + + + + {/* SECÇÃO: Sistema */} + + Sistema + + router.push('/Professor/defenicoes2')} cores={cores} corDestaque={cores.textoSecundario} /> + router.push('/Professor/PerfilProf')} cores={cores} corDestaque={cores.textoSecundario} /> + - - Estágios+ • EPVC 2026 + + Estágios+ • Versão 26.5.4 + ); } -function MenuCard({ icon, title, subtitle, onPress, cores }: any) { +// COMPONENTE DE CARTÃO REFORMULADO +function MenuCard({ icon, title, subtitle, onPress, cores, corDestaque, fullWidth = false }: any) { + const { isDarkMode } = useTheme(); // CORREÇÃO APLICADA AQUI + return ( - - + {/* Marca de água no fundo para design premium */} + + + + - + + {title} {subtitle} - - - ); } const styles = StyleSheet.create({ - content: { padding: 24 }, - header: { marginBottom: 35 }, - headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - welcome: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }, - name: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8, marginTop: 2 }, - avatarMini: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 8 }, - avatarTxt: { color: '#fff', fontSize: 22, fontWeight: '900' }, - infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 16, borderWidth: 1, borderStyle: 'dashed' }, - bannerTxt: { fontSize: 13, fontWeight: '700', marginLeft: 10 }, + content: { padding: 20 }, + + // Header + header: { marginBottom: 25, marginTop: 10 }, + headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, + badgeEpvc: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#E0F2F4', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 8 }, + badgeTxt: { fontSize: 10, fontWeight: '900', letterSpacing: 1, marginLeft: 4 }, + name: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 }, + subtitle: { fontSize: 14, fontWeight: '500', marginTop: 2 }, + avatarMini: { width: 56, height: 56, borderRadius: 20, justifyContent: 'center', alignItems: 'center', borderWidth: 2 }, + avatarTxt: { fontSize: 24, fontWeight: '900' }, + + // Hero Card (Novo Registo) + heroCard: { borderRadius: 28, padding: 24, minHeight: 140, justifyContent: 'flex-end', overflow: 'hidden', elevation: 8, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, marginBottom: 30 }, + heroWatermark: { position: 'absolute', right: -20, top: -20, opacity: 0.2, transform: [{ rotate: '15deg' }] }, + heroContent: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' }, + heroTitle: { color: '#FFF', fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, + heroSubtitle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 13, fontWeight: '600', marginTop: 4 }, + heroBtn: { width: 44, height: 44, backgroundColor: '#FFF', borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + + // Secções e Grid + sectionContainer: { marginBottom: 25 }, + sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12, marginLeft: 4 }, grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, + + // Cartões Menores card: { - width: (width - 64) / 2, borderRadius: 24, - padding: 20, + padding: 18, marginBottom: 16, borderWidth: 1, + overflow: 'hidden', + elevation: 2, shadowColor: '#000', - shadowOpacity: 0.03, - shadowRadius: 15, - elevation: 3, + shadowOpacity: 0.04, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 } }, - iconBox: { width: 48, height: 48, borderRadius: 15, justifyContent: 'center', alignItems: 'center', marginBottom: 16 }, - cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.2 }, - cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600', opacity: 0.7 }, - arrowCircle: { position: 'absolute', top: 20, right: 15, width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' }, - footer: { marginTop: 30, alignItems: 'center' }, - footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 }, + cardWatermark: { position: 'absolute', right: -15, bottom: -15, transform: [{ rotate: '-10deg' }] }, + iconWrapper: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginBottom: 16 }, + cardTextContainer: { marginTop: 'auto' }, + cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600' }, + + // Footer + footer: { marginTop: 20, alignItems: 'center', paddingBottom: 20 }, footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 } }); \ No newline at end of file diff --git a/app/Professor/defenicoes2.tsx b/app/Professor/defenicoes2.tsx index ea42b86..8a2d459 100644 --- a/app/Professor/defenicoes2.tsx +++ b/app/Professor/defenicoes2.tsx @@ -1,7 +1,7 @@ // app/Definicoes.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useCallback, useMemo, useState } from 'react'; +import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { Animated, Linking, @@ -23,34 +23,38 @@ const Definicoes = memo(() => { const { isDarkMode, toggleTheme } = useTheme(); const [notificacoes, setNotificacoes] = useState(true); - const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); - const alertOpacity = useMemo(() => new Animated.Value(0), []); + // 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 showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => { - setAlertConfig({ msg, type }); - Animated.sequence([ - Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }), - Animated.delay(2500), - Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) - ]).start(() => setAlertConfig(null)); - }, []); + 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' : '#FFFFFF', // Branco puro do login - card: isDarkMode ? '#161618' : '#F8FAFC', - texto: isDarkMode ? '#F8FAFC' : '#1A365D', - secundario: isDarkMode ? '#94A3B8' : '#718096', + 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)' : '#F0F9FA', - vermelho: '#EF4444', - vermelhoSuave: '#FFF5F5', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#38A169', + vermelho: erroCor, + verde: sucessoCor, + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2', }), [isDarkMode]); const handleLogout = async () => { @@ -58,40 +62,43 @@ const Definicoes = memo(() => { await supabase.auth.signOut(); router.replace('/'); } catch (e) { - showAlert("Erro ao sair da conta", "error"); + showToast("Erro ao sair da conta", "error"); } }; const abrirURL = (url: string) => { Linking.canOpenURL(url).then(supported => { if (supported) Linking.openURL(url); - else showAlert("Não foi possível abrir o link", "error"); + else showToast("Não foi possível abrir o link", "error"); }); }; return ( - + - {alertConfig && ( - - - {alertConfig.msg} - - )} + {/* 🟢 TOAST ANIMADO NO TOPO */} + + + {toast.message} + - + + {/* HEADER MODERNO */} router.back()} > @@ -100,7 +107,7 @@ const Definicoes = memo(() => { Definições Sistema & Suporte - + @@ -116,9 +123,9 @@ const Definicoes = memo(() => { value={notificacoes} onValueChange={(v) => { setNotificacoes(v); - showAlert(v ? "Notificações ligadas" : "Notificações desligadas", "info"); + showToast(v ? "Notificações ligadas" : "Notificações desligadas", "info"); }} - trackColor={{ false: '#CBD5E1', true: cores.laranja }} + trackColor={{ false: cores.borda, true: cores.laranja }} thumbColor="#FFFFFF" /> @@ -131,7 +138,7 @@ const Definicoes = memo(() => { @@ -150,7 +157,7 @@ const Definicoes = memo(() => { abrirURL('mailto:secretaria@epvc.pt')} cores={cores} showBorder @@ -173,7 +180,7 @@ const Definicoes = memo(() => { 2.6.17 - + @@ -198,6 +205,7 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) @@ -206,7 +214,7 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) {label} {subLabel} - + @@ -214,23 +222,28 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) const styles = StyleSheet.create({ safe: { flex: 1 }, - alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 }, - alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1, fontSize: 13 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + // 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: 22, fontWeight: '900', letterSpacing: -0.5 }, - tituloSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, - scrollContent: { paddingHorizontal: 24, paddingBottom: 60 }, - sectionLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, marginLeft: 4, letterSpacing: 1 }, - card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10, elevation: 2 }, + 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: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, - versionBadge: { fontSize: 11, fontWeight: '900', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, - arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + 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: 30, height: 3, borderRadius: 2, marginBottom: 15 }, - footerText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 } + footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 }, + footerText: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 } }); export default Definicoes; \ No newline at end of file diff --git a/app/Professor/redefenirsenha2.tsx b/app/Professor/redefenirsenha2.tsx index f43e670..764b087 100644 --- a/app/Professor/redefenirsenha2.tsx +++ b/app/Professor/redefenirsenha2.tsx @@ -1,9 +1,10 @@ -// app/forgot-password.tsx +// app/Professor/redefenirsenha2.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, + Animated, KeyboardAvoidingView, Platform, ScrollView, @@ -14,297 +15,227 @@ import { TouchableOpacity, View, } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; +import { useTheme } from '../../themecontext'; -export default function ForgotPassword() { - const [email, setEmail] = useState(''); - const [loading, setLoading] = useState(false); - const [isFocused, setIsFocused] = useState(false); - - // ESTADOS PARA OS AVISOS MODERNOS - const [status, setStatus] = useState<{ type: 'error' | 'success'; msg: string } | null>(null); - +export default function RedefinirSenhaInterna() { const router = useRouter(); + const { isDarkMode } = useTheme(); + const insets = useSafeAreaInsets(); - const handleSendResetEmail = async () => { - setStatus(null); // Limpa avisos anteriores + const [novaPassword, setNovaPassword] = useState(''); + const [confirmarPassword, setConfirmarPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [isFocused1, setIsFocused1] = useState(false); + const [isFocused2, setIsFocused2] = useState(false); + const [showPassword, setShowPassword] = useState(false); - if (!email) { - setStatus({ type: 'error', msg: 'Por favor, insira o seu email corretamente.' }); + // ESTADOS PARA O TOAST ANIMADO + const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' }>({ visible: false, message: '', type: 'error' }); + const slideAnim = useRef(new Animated.Value(-100)).current; + + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + const erroCor = '#EF4444'; + const sucessoCor = '#10B981'; + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', + 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', + inputFundo: isDarkMode ? '#252525' : '#FBFDFF', + placeholder: isDarkMode ? '#555' : '#94A3B8', + verde: sucessoCor // 🟢 COR ADICIONADA AQUI + }), [isDarkMode]); + + const showToast = useCallback((message: string, type: 'error' | 'success' = 'error') => { + 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: 'error' })); + }, 3500); + }); + }, [insets.top, slideAnim]); + + const handleUpdatePassword = async () => { + if (!novaPassword || !confirmarPassword) { + showToast("Preenche todos os campos!", 'error'); + return; + } + + if (novaPassword.length < 6) { + showToast("A palavra-passe tem de ter pelo menos 6 caracteres.", 'error'); + return; + } + + if (novaPassword !== confirmarPassword) { + showToast("As palavras-passe não coincidem.", 'error'); return; } setLoading(true); try { - const { error } = await supabase.auth.resetPasswordForEmail(email); + const { error } = await supabase.auth.updateUser({ password: novaPassword }); + if (error) throw error; - setStatus({ type: 'success', msg: 'Link enviado! Verifique ao seu email.' }); + showToast("Palavra-passe atualizada com segurança!", 'success'); - // Espera 3 segundos para o utilizador ler e volta para o login - setTimeout(() => router.back(), 3500); + setTimeout(() => router.back(), 2000); } catch (err: any) { - setStatus({ type: 'error', msg: 'Não foi possível enviar o email. Tente novamente!' }); + showToast(err.message, 'error'); } finally { setLoading(false); } }; return ( - - - - - router.back()} - style={styles.backButtonTop} - > - - - - Voltar - + + + + {/* TOAST ANIMADO NO TOPO */} + + + {toast.message} + - - - - - + + + + + router.back()} style={[styles.backBtn, { borderColor: cores.borda, backgroundColor: cores.card }]}> + + + + Segurança + Estágios+ + + + + + + + + + + + - - - Não sabes a tua palavra-passe? - - Não te preocupes! Insere o teu email e enviaremos instruções para recuperares o acesso à nossa app. + Alterar Palavra-passe + + Cria uma palavra-passe forte para manteres a tua conta protegida. - {/* AVISOS/ERROS MODERNOS AQUI */} - {status && ( - - - - {status.msg} - - - )} - + - Email - { - setEmail(val); - if(status) setStatus(null); // Limpa o erro ao começar a digitar - }} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} - keyboardType="email-address" - autoCapitalize="none" - editable={!loading} - /> + Nova Palavra-passe + + + setIsFocused1(true)} + onBlur={() => setIsFocused1(false)} + secureTextEntry={!showPassword} + editable={!loading} + /> + setShowPassword(!showPassword)} style={{ padding: 5 }}> + + + + + + + Confirmar Palavra-passe + + + setIsFocused2(true)} + onBlur={() => setIsFocused2(false)} + secureTextEntry={!showPassword} + editable={!loading} + /> + {loading ? ( ) : ( - Enviar Instruções + + + Atualizar Palavra-passe + )} - - - EPVC Estágios+ • 2026 - - - + + + ); } const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - backgroundColor: '#FFFFFF', - }, - scrollContainer: { - flexGrow: 1, - paddingHorizontal: 28, - paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 20 : 60, - paddingBottom: 40, - }, - backButtonTop: { - flexDirection: 'row', - alignItems: 'center', - alignSelf: 'flex-start', - marginBottom: 30, - }, - backIconCircle: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: '#F8FAFC', - justifyContent: 'center', - alignItems: 'center', - marginRight: 10, - borderWidth: 1, - borderColor: '#F1F5F9', - }, - backButtonText: { - fontSize: 15, - color: '#64748B', - fontWeight: '600', - }, - content: { - flex: 1, - justifyContent: 'center', - }, - header: { - alignItems: 'center', - marginBottom: 35, - }, - iconWrapper: { - position: 'relative', - marginBottom: 20, - }, - iconCircle: { - width: 90, - height: 90, - backgroundColor: '#F0F7FF', - borderRadius: 30, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 1, - borderColor: '#DBEAFE', - }, - iconBadge: { - position: 'absolute', - bottom: -4, - right: -4, - width: 20, - height: 20, - borderRadius: 10, - backgroundColor: '#2390a6', - borderWidth: 3, - borderColor: '#FFFFFF', - }, - title: { - fontSize: 28, - fontWeight: '900', - color: '#0F172A', - textAlign: 'center', - letterSpacing: -1, - }, - subtitle: { - fontSize: 15, - color: '#64748B', - textAlign: 'center', - marginTop: 10, - lineHeight: 22, - maxWidth: 300, - }, - // ESTILOS DOS BANNERS MODERNOS - statusBanner: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - borderRadius: 16, - marginBottom: 25, - borderWidth: 1, - }, - statusText: { - fontSize: 14, - fontWeight: '600', - marginLeft: 10, - flex: 1, - }, - errorBg: { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' }, - errorText: { color: '#B91C1C' }, - successBg: { backgroundColor: '#F0FDF4', borderColor: '#DCFCE7' }, - successText: { color: '#166534' }, + safe: { flex: 1 }, + // TOAST STYLES + toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 100, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, + toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 10, flex: 1 }, - form: { - width: '100%', - }, - inputWrapper: { - marginBottom: 24, - }, - label: { - fontSize: 14, - fontWeight: '700', - color: '#475569', - marginBottom: 10, - marginLeft: 4, - }, - input: { - backgroundColor: '#FBFDFF', - borderRadius: 18, - paddingHorizontal: 20, - paddingVertical: 18, - fontSize: 16, - color: '#0F172A', - borderWidth: 1.5, - borderColor: '#F1F5F9', - }, - inputFocused: { - borderColor: '#2390a6', - backgroundColor: '#FFFFFF', - }, - button: { - backgroundColor: '#dd8707', - borderRadius: 18, - paddingVertical: 20, - alignItems: 'center', - shadowColor: '#dd8707', - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.3, - shadowRadius: 10, - elevation: 6, - }, - buttonDisabled: { - backgroundColor: '#E2E8F0', - elevation: 0, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: '800', - }, - footer: { - marginTop: 40, - alignItems: 'center', - }, - footerText: { - fontSize: 12, - color: '#CBD5E1', - fontWeight: '600', - }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + backBtn: { width: 44, height: 44, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginTop: 2 }, // 🟢 ESTILO ADICIONADO AQUI + + scrollContent: { paddingHorizontal: 28, paddingBottom: 40 }, + + heroSection: { alignItems: 'center', marginTop: 30, marginBottom: 40 }, + iconShield: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5, position: 'relative' }, + badgeVerificado: { position: 'absolute', bottom: -5, right: -5, width: 24, height: 24, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: '#FFF' }, + title: { fontSize: 26, fontWeight: '900', marginTop: 20, letterSpacing: -0.5 }, + subtitle: { fontSize: 14, textAlign: 'center', marginTop: 8, lineHeight: 22, fontWeight: '500' }, + + form: { width: '100%' }, + inputWrapper: { marginBottom: 20 }, + label: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + input: { flex: 1, fontSize: 15, fontWeight: '600' }, + + button: { borderRadius: 18, height: 60, justifyContent: 'center', alignItems: 'center', marginTop: 15, elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 }, + buttonText: { color: '#fff', fontSize: 15, fontWeight: '900', letterSpacing: 1 }, }); \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 9828b69..8d3db94 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -28,6 +28,7 @@ export default function LoginScreen() { return; } + // Vai à base de dados perguntar quem é esta pessoa const { data, error } = await supabase .from('profiles') .select('tipo') @@ -44,10 +45,13 @@ export default function LoginScreen() { return; } + // O Polícia Sinaleiro: Manda cada um para o seu lado if (data.tipo === 'professor') { router.replace('/Professor/ProfessorHome'); } else if (data.tipo === 'aluno') { router.replace('/Aluno/AlunoHome'); + } else if (data.tipo === 'empresa') { + router.replace('/Empresas/EmpresaHome'); // 🟢 Rota da empresa adicionada! } else { Alert.alert('Erro', 'Tipo de conta inválido'); } @@ -123,14 +127,14 @@ const styles = StyleSheet.create({ }, header: { alignItems: 'center', - marginBottom: 20, // Reduzi para o bloco de texto ficar mais perto do Auth + marginBottom: 20, }, logoContainer: { width: 420, height: 220, justifyContent: 'center', alignItems: 'center', - marginBottom: -60, // MARGEM NEGATIVA: Aproxima o texto da imagem ao máximo + marginBottom: -60, }, logoImage: { width: '100%', @@ -149,7 +153,7 @@ const styles = StyleSheet.create({ fontSize: 16, color: '#64748B', textAlign: 'center', - marginTop: 2, // Reduzi para o subtítulo ficar colado ao título principal + marginTop: 2, fontWeight: '500', lineHeight: 24, maxWidth: 280,