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