atualizacoes

This commit is contained in:
2026-05-05 21:44:13 +01:00
parent 01f178a21e
commit 9097bfabbd
4 changed files with 298 additions and 22 deletions

View File

@@ -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];

View File

@@ -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 (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]} edges={['top', 'left', 'right']}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={themeStyles.fundo} />
{/* CABEÇALHO */}
<View style={styles.topBar}>
<View>
<View style={{ flex: 1, paddingRight: 20 }}>
<Text style={[styles.greeting, { color: themeStyles.textoSecundario }]}>Painel da Entidade</Text>
{loading ? (
<ActivityIndicator size="small" color={themeStyles.azul} style={{ marginTop: 5, alignSelf: 'flex-start' }} />
) : (
<Text style={[styles.title, { color: themeStyles.texto }]}>{empresaNome || 'A carregar...'}</Text>
<Text style={[styles.title, { color: themeStyles.texto }]} numberOfLines={1}>
{empresaNome || 'A carregar...'}
</Text>
)}
</View>
<TouchableOpacity style={[styles.logoutBtn, { borderColor: themeStyles.borda }]} onPress={() => 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' },

View File

@@ -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,

View File

@@ -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<any>(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 (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: themeStyles.texto }]}>Avaliar Registo</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
{estagioInfo && (
<View style={[styles.estagioCard, { backgroundColor: themeStyles.azul + '10', borderColor: themeStyles.azul + '30' }]}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 8 }}>
<Ionicons name="analytics" size={20} color={themeStyles.azul} />
<Text style={{ fontSize: 13, fontWeight: '900', color: themeStyles.azul, textTransform: 'uppercase', letterSpacing: 0.5 }}>
Progresso Atual do Aluno
</Text>
</View>
<View style={styles.infoRow}>
<View style={styles.infoCol}>
<Text style={[styles.labelContext, { color: themeStyles.textoSecundario }]}>HORAS CONCLUÍDAS</Text>
<Text style={[styles.valueContext, { color: themeStyles.texto }]}>
{estagioInfo.horas_concluidas} <Text style={{ fontSize: 14, color: themeStyles.textoSecundario }}>/ {estagioInfo.horas_totais}h</Text>
</Text>
</View>
<View style={styles.infoCol}>
<Text style={[styles.labelContext, { color: themeStyles.textoSecundario }]}>PERÍODO DE ESTÁGIO</Text>
<Text style={[styles.valueContext, { color: themeStyles.texto, fontSize: 13 }]}>
{formatarData(estagioInfo.data_inicio)} a {formatarData(estagioInfo.data_fim)}
</Text>
</View>
</View>
</View>
)}
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, elevation: isDarkMode ? 0 : 2, shadowOpacity: isDarkMode ? 0 : 0.05 }]}>
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>NOME DO ALUNO</Text>
<Text style={[styles.value, { color: themeStyles.texto }]}>
<Ionicons name="person" size={16} color={themeStyles.textoSecundario} /> {aluno_nome}
</Text>
<View style={[styles.divider, { backgroundColor: themeStyles.borda }]} />
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>DATA SUBMETIDA</Text>
<Text style={[styles.value, { color: themeStyles.texto }]}>
<Ionicons name="calendar" size={16} color={themeStyles.textoSecundario} /> {formatarData(String(data))}
</Text>
<View style={[styles.divider, { backgroundColor: themeStyles.borda }]} />
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>CLASSIFICAÇÃO DO REGISTO</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 6 }}>
<Ionicons name={tipoInfo.icone} size={22} color={tipoInfo.cor} />
<Text style={{ fontSize: 16, fontWeight: '800', color: tipoInfo.cor }}>
{tipoInfo.texto}
</Text>
</View>
</View>
<Text style={[styles.sectionTitle, { color: themeStyles.texto, marginTop: 15 }]}>
{isFalta ? 'Detalhes da Ausência' : 'Sumário das Atividades'}
</Text>
<View style={[styles.descCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, elevation: isDarkMode ? 0 : 2, shadowOpacity: isDarkMode ? 0 : 0.05 }]}>
{isFalta ? (
<View>
{hasUrl ? (
<View style={styles.anexoContainer}>
<Text style={[styles.sumarioText, { color: themeStyles.texto, marginBottom: 15, textAlign: 'center' }]}>
O aluno submeteu um documento para justificar esta falta. Verifique a validade do anexo abaixo.
</Text>
<TouchableOpacity activeOpacity={0.8} style={[styles.anexoBtn, { backgroundColor: themeStyles.azul }]} onPress={abrirJustificacao}>
<Ionicons name="cloud-download-outline" size={24} color="#FFF" />
<Text style={[styles.anexoText, { color: '#FFF' }]}>Visualizar Atestado Médico</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.avisoContainer}>
<Ionicons name="warning" size={32} color={themeStyles.vermelho} style={{ marginBottom: 10 }} />
<Text style={[styles.sumarioText, { color: themeStyles.vermelho, fontWeight: '700', textAlign: 'center' }]}>
O aluno marcou uma falta mas não anexou nenhuma justificação oficial.
</Text>
</View>
)}
</View>
) : (
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
{sumario && String(sumario) !== 'null' && String(sumario).trim() !== '' ? String(sumario) : <Text style={{ color: themeStyles.textoSecundario, fontStyle: 'italic' }}>Nenhum sumário foi preenchido pelo aluno para este dia.</Text>}
</Text>
)}
</View>
</ScrollView>
<View style={[styles.footer, { backgroundColor: themeStyles.card, borderTopColor: themeStyles.borda }]}>
<TouchableOpacity
activeOpacity={0.8}
style={[styles.btnAction, { backgroundColor: themeStyles.fundo, borderColor: themeStyles.vermelho, borderWidth: 1.5 }]}
onPress={() => handleValidar('rejeitado')}
disabled={loadingAprovar || loadingRejeitar}
>
{loadingRejeitar ? <ActivityIndicator color={themeStyles.vermelho} /> : <Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>}
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.8}
style={[styles.btnAction, { backgroundColor: themeStyles.verde, borderColor: themeStyles.verde, borderWidth: 1.5 }]}
onPress={() => handleValidar('aprovado')}
disabled={loadingAprovar || loadingRejeitar}
>
{loadingAprovar ? <ActivityIndicator color="#FFF" /> : <Text style={[styles.btnActionText, { color: '#FFF' }]}>Aprovar Registo</Text>}
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 15 },
btnVoltar: { padding: 5, marginLeft: -5 },
headerTitle: { fontSize: 20, fontWeight: '900' },
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' }
});