From 9097bfabbdff5cb17c7abc0f69bc13e957794c60 Mon Sep 17 00:00:00 2001 From: Seu Nome <230413@epvc.pt> Date: Tue, 5 May 2026 21:44:13 +0100 Subject: [PATCH] atualizacoes --- app/Aluno/AlunoHome.tsx | 3 +- app/Empresas/EmpresaHome.tsx | 17 +- app/Empresas/pedidos.tsx | 24 +-- app/Empresas/validarPedido.tsx | 276 +++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 22 deletions(-) create mode 100644 app/Empresas/validarPedido.tsx diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index 3162140..996f506 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -431,8 +431,7 @@ const badgeObj = getBadgeStyle(); }; const horasTotais = Number(estagioDetalhes?.horas_totais) || 0; - const horasPorDia = Number(estagioDetalhes?.horas_diarias) || 0; - const horasConcluidas = (statsFaltas?.totalPresencas || 0) * horasPorDia; + const horasConcluidas = Number(estagioDetalhes?.horas_concluidas) || 0; const horasEmFalta = Math.max(0, horasTotais - horasConcluidas); const regSelecionado = registosDiarios[selectedDate]; diff --git a/app/Empresas/EmpresaHome.tsx b/app/Empresas/EmpresaHome.tsx index 5339c38..7694f66 100644 --- a/app/Empresas/EmpresaHome.tsx +++ b/app/Empresas/EmpresaHome.tsx @@ -5,8 +5,6 @@ import { useRouter } from 'expo-router'; import { useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, - Platform, - SafeAreaView, ScrollView, StatusBar, StyleSheet, @@ -14,6 +12,7 @@ import { TouchableOpacity, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; @@ -60,17 +59,19 @@ export default function EmpresaHome() { useFocusEffect(useCallback(() => { fetchEmpresaInfo(); }, [])); return ( - - + + {/* CABEÇALHO */} - + Painel da Entidade {loading ? ( ) : ( - {empresaNome || 'A carregar...'} + + {empresaNome || 'A carregar...'} + )} supabase.auth.signOut().then(() => router.replace('/'))}> @@ -153,8 +154,8 @@ export default function EmpresaHome() { } const styles = StyleSheet.create({ - safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 15 }, + safeArea: { flex: 1 }, + topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 10, paddingBottom: 15 }, greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, title: { fontSize: 24, fontWeight: '900', marginTop: 2 }, logoutBtn: { width: 45, height: 45, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, diff --git a/app/Empresas/pedidos.tsx b/app/Empresas/pedidos.tsx index 4dd3808..6430ec0 100644 --- a/app/Empresas/pedidos.tsx +++ b/app/Empresas/pedidos.tsx @@ -4,17 +4,17 @@ 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 + ActivityIndicator, + Alert, + Platform, + RefreshControl, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; import { supabase } from '../../lib/supabase'; import { useTheme } from '../../themecontext'; @@ -162,7 +162,7 @@ export default function PedidosPendentes() { style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: tipo.cor }]} onPress={() => { router.push({ - pathname: '/Empresa/validar-registo', + pathname: '/Empresas/validarPedido', params: { aluno_id: item.aluno_id, aluno_nome: item.aluno_nome, diff --git a/app/Empresas/validarPedido.tsx b/app/Empresas/validarPedido.tsx new file mode 100644 index 0000000..adcbfe1 --- /dev/null +++ b/app/Empresas/validarPedido.tsx @@ -0,0 +1,276 @@ +// app/Empresas/validar-registo.tsx +import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { useEffect, useMemo, useState } from 'react'; +import { + ActivityIndicator, + Alert, + Linking, + Platform, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; +import { supabase } from '../../lib/supabase'; +import { useTheme } from '../../themecontext'; + +export default function ValidarRegisto() { + const { isDarkMode } = useTheme(); + const router = useRouter(); + const params = useLocalSearchParams(); + + const aluno_id = Array.isArray(params.aluno_id) ? params.aluno_id[0] : params.aluno_id; + const aluno_nome = Array.isArray(params.aluno_nome) ? params.aluno_nome[0] : params.aluno_nome; + const data = Array.isArray(params.data) ? params.data[0] : params.data; + const sumario = Array.isArray(params.sumario) ? params.sumario[0] : params.sumario; + const estadoRaw = Array.isArray(params.estado) ? params.estado[0] : params.estado; + const justificacao_url = Array.isArray(params.justificacao_url) ? params.justificacao_url[0] : params.justificacao_url; + + const estado = String(estadoRaw || '').toLowerCase().trim(); + + // 🟢 A GRANDE MUDANÇA: Se não é 'presente', é garantidamente tratado como ausência! + const isFalta = estado !== 'presente'; + + const [loadingAprovar, setLoadingAprovar] = useState(false); + const [loadingRejeitar, setLoadingRejeitar] = useState(false); + const [estagioInfo, setEstagioInfo] = useState(null); + + const themeStyles = useMemo(() => ({ + fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', + card: isDarkMode ? '#1A1A1A' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + azul: '#2390a6', + verde: '#10B981', + vermelho: '#EF4444', + laranja: '#dd8707', + }), [isDarkMode]); + + useEffect(() => { + const fetchEstagio = async () => { + if (!aluno_id) return; + const { data: estagio } = await supabase + .from('estagios') + .select('horas_totais, horas_concluidas, data_inicio, data_fim') + .eq('aluno_id', aluno_id) + .single(); + + if (estagio) setEstagioInfo(estagio); + }; + fetchEstagio(); + }, [aluno_id]); + + const formatarData = (dataStr: string) => { + if (!dataStr) return ''; + const parts = dataStr.split('-'); + if (parts.length !== 3) return dataStr; + return `${parts[2]}/${parts[1]}/${parts[0]}`; + }; + + const handleValidar = async (novoEstado: 'aprovado' | 'rejeitado') => { + if (novoEstado === 'aprovado') setLoadingAprovar(true); + else setLoadingRejeitar(true); + + try { + const { error } = await supabase + .from('presencas') + .update({ estado_tutor: novoEstado }) + .eq('aluno_id', aluno_id) + .eq('data', data); + + if (error) throw error; + + Alert.alert('Sucesso', `O registo foi ${novoEstado} com sucesso!`); + router.back(); + } catch (error) { + console.error(error); + Alert.alert('Erro', 'Não foi possível processar a validação.'); + } finally { + setLoadingAprovar(false); + setLoadingRejeitar(false); + } + }; + + const hasUrl = justificacao_url && justificacao_url !== 'null' && String(justificacao_url).trim() !== ''; + + const abrirJustificacao = () => { + if (!hasUrl) { + Alert.alert('Aviso', 'Não existe nenhum ficheiro associado a este registo.'); + return; + } + Linking.openURL(String(justificacao_url)).catch((err) => { + console.error("Erro ao abrir link:", err); + Alert.alert('Erro', 'Ocorreu uma falha ao tentar abrir o documento do aluno.'); + }); + }; + + const getTipoRegistoInfo = () => { + if (!isFalta) { + return { texto: 'Presença Regular', cor: themeStyles.verde, icone: 'checkmark-circle' as const }; + } + if (hasUrl) { + return { texto: 'Falta Justificada (Com Anexo)', cor: themeStyles.laranja, icone: 'document-attach' as const }; + } + return { texto: 'Falta Injustificada', cor: themeStyles.vermelho, icone: 'close-circle' as const }; + }; + + const tipoInfo = getTipoRegistoInfo(); + + return ( + + + + + router.back()}> + + + Avaliar Registo + + + + + + {estagioInfo && ( + + + + + Progresso Atual do Aluno + + + + + HORAS CONCLUÍDAS + + {estagioInfo.horas_concluidas} / {estagioInfo.horas_totais}h + + + + PERÍODO DE ESTÁGIO + + {formatarData(estagioInfo.data_inicio)} a {formatarData(estagioInfo.data_fim)} + + + + + )} + + + NOME DO ALUNO + + {aluno_nome} + + + + + DATA SUBMETIDA + + {formatarData(String(data))} + + + + + CLASSIFICAÇÃO DO REGISTO + + + + {tipoInfo.texto} + + + + + + {isFalta ? 'Detalhes da Ausência' : 'Sumário das Atividades'} + + + + {isFalta ? ( + + {hasUrl ? ( + + + O aluno submeteu um documento para justificar esta falta. Verifique a validade do anexo abaixo. + + + + Visualizar Atestado Médico + + + ) : ( + + + + O aluno marcou uma falta mas não anexou nenhuma justificação oficial. + + + )} + + ) : ( + + {sumario && String(sumario) !== 'null' && String(sumario).trim() !== '' ? String(sumario) : Nenhum sumário foi preenchido pelo aluno para este dia.} + + )} + + + + + + handleValidar('rejeitado')} + disabled={loadingAprovar || loadingRejeitar} + > + {loadingRejeitar ? : Rejeitar} + + + handleValidar('aprovado')} + disabled={loadingAprovar || loadingRejeitar} + > + {loadingAprovar ? : Aprovar Registo} + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, + header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 15 }, + btnVoltar: { padding: 5, marginLeft: -5 }, + headerTitle: { fontSize: 20, fontWeight: '900' }, + scroll: { paddingHorizontal: 20, paddingBottom: 40 }, + + estagioCard: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 20 }, + infoRow: { flexDirection: 'row', justifyContent: 'space-between' }, + infoCol: { flex: 1 }, + labelContext: { fontSize: 10, fontWeight: '800', letterSpacing: 0.5, marginBottom: 6 }, + valueContext: { fontSize: 16, fontWeight: '900' }, + + card: { padding: 22, borderRadius: 24, borderWidth: 1, marginBottom: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowRadius: 10 }, + label: { fontSize: 10, fontWeight: '800', letterSpacing: 1, marginBottom: 6 }, + value: { fontSize: 16, fontWeight: '700' }, + divider: { height: 1, marginVertical: 18, opacity: 0.5 }, + + sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5, marginBottom: 12, marginLeft: 5, textTransform: 'uppercase' }, + descCard: { padding: 22, borderRadius: 24, borderWidth: 1, minHeight: 140, justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowRadius: 10 }, + sumarioText: { fontSize: 15, lineHeight: 24, fontWeight: '500' }, + + anexoContainer: { alignItems: 'center' }, + anexoBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 16, paddingHorizontal: 20, borderRadius: 16, gap: 12, width: '100%' }, + anexoText: { fontSize: 15, fontWeight: '800' }, + + avisoContainer: { alignItems: 'center', paddingVertical: 10 }, + + footer: { flexDirection: 'row', gap: 15, padding: 20, paddingBottom: Platform.OS === 'ios' ? 30 : 20, borderTopWidth: 1 }, + btnAction: { flex: 1, paddingVertical: 18, borderRadius: 18, alignItems: 'center', justifyContent: 'center' }, + btnActionText: { fontSize: 16, fontWeight: '800' } +}); \ No newline at end of file