This commit is contained in:
2026-05-12 17:17:43 +01:00
parent f8d12176fa
commit e7ab95c2ea
2 changed files with 177 additions and 271 deletions

View File

@@ -6,7 +6,7 @@ import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system/legacy';
import * as Location from 'expo-location';
import { useRouter } from 'expo-router';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
@@ -50,14 +50,12 @@ const AlunoHome = memo(() => {
const hojeStr = new Date().toISOString().split('T')[0];
const scrollViewRef = useRef<ScrollView>(null);
// ESTADO DOS SEPARADORES
const [activeTab, setActiveTab] = useState<'horas' | 'horario' | 'info'>('horas');
const [activeTab, setActiveTab] = useState<'assiduidade' | 'horario' | 'info'>('assiduidade');
const [selectedDate, setSelectedDate] = useState(hojeStr);
const [userRole, setUserRole] = useState('aluno');
const [estagioDetalhes, setEstagioDetalhes] = useState<any>(null);
const [horariosEstagio, setHorariosEstagio] = useState<any[]>([]);
// 🟢 O NOVO MOTOR CENTRAL (Substitui as 5 variáveis antigas)
const [registosDiarios, setRegistosDiarios] = useState<Record<string, any>>({});
const [statsFaltas, setStatsFaltas] = useState({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
@@ -73,19 +71,16 @@ const AlunoHome = memo(() => {
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
const alertOpacity = useMemo(() => new Animated.Value(0), []);
const azulPetroleo = '#2390a6';
const laranjaEPVC = '#dd8707';
const themeStyles = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: azulPetroleo,
laranja: laranjaEPVC,
amarelo: '#F59E0B', // Adicionado para as faltas por confirmar
cinzento: '#94A3B8', // Adicionado para presenças por confirmar
azul: '#2390a6',
laranja: '#dd8707',
amarelo: '#F59E0B',
cinzento: '#94A3B8',
verde: '#10B981',
vermelho: '#EF4444',
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
@@ -109,9 +104,12 @@ const AlunoHome = memo(() => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: profile } = await supabase.from('profiles').select('tipo').eq('id', user.id).single();
if (profile) setUserRole(profile.tipo);
const { data: eData } = await supabase
.from('estagios')
.select('id, data_inicio, data_fim, horas_totais, horas_concluidas, horas_diarias, empresas(nome, tutor_nome, tutor_telefone)')
.select('id, data_inicio, data_fim, horas_diarias, empresas(nome, tutor_nome, tutor_telefone)')
.eq('aluno_id', user.id)
.order('data_fim', { ascending: false })
.limit(1)
@@ -120,11 +118,7 @@ const AlunoHome = memo(() => {
setEstagioDetalhes(eData || null);
if (eData && eData.id) {
const { data: hData } = await supabase
.from('horarios_estagio')
.select('periodo, hora_inicio, hora_fim')
.eq('estagio_id', eData.id);
const { data: hData } = await supabase.from('horarios_estagio').select('periodo, hora_inicio, hora_fim').eq('estagio_id', eData.id);
setHorariosEstagio(hData || []);
} else {
setHorariosEstagio([]);
@@ -145,22 +139,17 @@ const AlunoHome = memo(() => {
data?.forEach(item => {
novosRegistos[item.data] = item;
if (item.estado === 'presente' && item.estado_tutor === 'aprovado') {
countPresencasAprovadas++;
} else if (item.estado === 'faltou') {
if (item.estado_tutor === 'aprovado' && item.justificacao_url) {
countJustificadas++;
} else if (item.estado_tutor === 'aprovado' || item.estado_tutor === 'rejeitado') {
countInjustificadas++;
}
if (item.estado_tutor === 'aprovado' && item.justificacao_url) countJustificadas++;
else if (item.estado_tutor === 'aprovado' || item.estado_tutor === 'rejeitado') countInjustificadas++;
}
});
setRegistosDiarios(novosRegistos);
setStatsFaltas({ justificadas: countJustificadas, injustificadas: countInjustificadas, totalPresencas: countPresencasAprovadas });
// Garante que o input do sumário reflete o dia atual
if (novosRegistos[selectedDate]) setSumarioInput(novosRegistos[selectedDate].sumario || "");
else setSumarioInput("");
@@ -168,7 +157,6 @@ const AlunoHome = memo(() => {
setRegistosDiarios({});
setStatsFaltas({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
}
} catch (error) {
console.error(error);
} finally {
@@ -178,23 +166,6 @@ const AlunoHome = memo(() => {
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, [selectedDate]));
useEffect(() => {
const estagiosSubscription = supabase
.channel('estagios_changes')
.on('postgres_changes', { event: '*', schema: 'public', table: 'estagios' }, () => fetchDadosSupabase())
.subscribe();
const presencasSubscription = supabase
.channel('presencas_changes')
.on('postgres_changes', { event: '*', schema: 'public', table: 'presencas' }, () => fetchDadosSupabase())
.subscribe();
return () => {
supabase.removeChannel(estagiosSubscription);
supabase.removeChannel(presencasSubscription);
};
}, []);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchDadosSupabase(true);
@@ -211,24 +182,20 @@ const AlunoHome = memo(() => {
}, [estagioDetalhes, hojeStr]);
const infoData = useMemo(() => {
const data = new Date(selectedDate);
const diaSemana = data.getDay();
const nomeFeriado = feriadosMap[selectedDate];
const temEstagio = !!estagioDetalhes && estagioDetalhes.data_inicio && estagioDetalhes.data_fim;
const antesDoInicio = temEstagio && selectedDate < estagioDetalhes.data_inicio;
const depoisDoFim = temEstagio && selectedDate > estagioDetalhes.data_fim;
const estagioAtivo = statusEstagio === 'ativo';
return {
valida: estagioAtivo && diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado,
valida: estagioAtivo && !antesDoInicio && !depoisDoFim && !nomeFeriado,
podeMarcar: estagioAtivo && selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado,
nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: !temEstagio || antesDoInicio || depoisDoFim,
temEstagio, estagioAtivo
};
}, [selectedDate, estagioDetalhes, hojeStr, feriadosMap, statusEstagio]);
// Modificado para ver no "cofre" em vez das antigas variáveis
const isDiaMarcado = () => !!registosDiarios[selectedDate];
const savePresencaData = async (payload: any, successMessage: string) => {
@@ -280,7 +247,7 @@ const AlunoHome = memo(() => {
lat: loc.coords.latitude,
lng: loc.coords.longitude,
estado_tutor: 'pendente'
}, "Presença marcada! A aguardar aprovação da empresa.");
}, "Presença registada! A aguardar aprovação da entidade.");
} catch (e: any) {
showAlert(e.message, "error");
@@ -296,7 +263,7 @@ const AlunoHome = memo(() => {
await savePresencaData({
estado: 'faltou',
estado_tutor: 'pendente'
}, "Falta registada e enviada para a entidade.");
}, "Falta registada e notificada à entidade.");
};
const selecionarDocumento = async () => {
@@ -316,11 +283,7 @@ const AlunoHome = memo(() => {
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
await savePresencaData({
justificacao_url: publicUrl,
estado_tutor: 'pendente'
}, "Justificativo enviado à entidade com sucesso!");
await savePresencaData({ justificacao_url: publicUrl, estado_tutor: 'pendente' }, "Documento submetido à entidade com sucesso!");
setPdf(null);
} catch (e) {
showAlert("Erro no upload do documento.", "error");
@@ -330,49 +293,37 @@ const AlunoHome = memo(() => {
};
const guardarSumario = async () => {
await savePresencaData({
sumario: sumarioInput,
estado_tutor: 'pendente'
}, "Sumário submetido para validação!");
await savePresencaData({ sumario: sumarioInput, estado_tutor: 'pendente' }, "Atividades submetidas para validação!");
setEditandoSumario(false);
};
// 🟢 AS CORES AGORA REFLETEM AS TUAS REGRAS EXATAS
const gerarMarcacoesCalendario = () => {
const marcacoes: any = {};
// Feriados ficam com o azul principal da app
Object.keys(feriadosMap).forEach(d => { marcacoes[d] = { marked: true, dotColor: '#000000b7' }; });
// Pontinhos Azuis: Identificar os dias que já passaram e que estão sem nada
if (estagioDetalhes?.data_inicio) {
const start = new Date(estagioDetalhes.data_inicio);
const limit = new Date(hojeStr) < new Date(estagioDetalhes.data_fim) ? new Date(hojeStr) : new Date(estagioDetalhes.data_fim);
for (let d = new Date(start); d <= limit; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const diaSemana = d.getDay();
if (diaSemana !== 0 && diaSemana !== 6 && !feriadosMap[dateStr]) {
if (!registosDiarios[dateStr]) {
marcacoes[dateStr] = { marked: true, dotColor: '#0947f1b7' }; // 🔵 Azul: Sem nada
}
if (!feriadosMap[dateStr] && !registosDiarios[dateStr]) {
marcacoes[dateStr] = { marked: true, dotColor: '#0947f1b7' };
}
}
}
// Regras de Cores para dias com Registo
Object.values(registosDiarios).forEach(reg => {
let cor = themeStyles.azul;
const temSumario = reg.sumario && reg.sumario.trim() !== '';
if (reg.estado === 'presente') {
if (reg.estado_tutor === 'aprovado' && temSumario) cor = themeStyles.verde; // 🟢 Verde: Confirmada e com sumário
else if (!temSumario) cor = themeStyles.amarelo; // 🟡 Amarelo: Presença sem sumário
else cor = themeStyles.azul; // 🔵 Azul: Tem sumário mas falta validar ("Sem nada" da empresa)
if (reg.estado_tutor === 'aprovado' && temSumario) cor = themeStyles.verde;
else if (!temSumario) cor = themeStyles.amarelo;
else cor = themeStyles.azul;
} else if (reg.estado === 'faltou') {
if (reg.estado_tutor === 'aprovado') cor = themeStyles.cinzento; // 🔘 Cinzento: Falta confirmada
else if (reg.justificacao_url) cor = themeStyles.amarelo; // 🟡 Amarelo: Falta justificada (ainda não aprovada)
else cor = themeStyles.vermelho; // 🔴 Vermelho: Falta injustificada
if (reg.estado_tutor === 'aprovado') cor = themeStyles.cinzento;
else if (reg.justificacao_url) cor = themeStyles.amarelo;
else cor = themeStyles.vermelho;
}
marcacoes[reg.data] = { marked: true, dotColor: cor };
});
@@ -384,42 +335,25 @@ const AlunoHome = memo(() => {
const getBadgeStyle = () => {
if (statusEstagio === 'concluido') return { bg: '#E2E8F0', text: '#475569', label: 'CONCLUÍDO' };
if (statusEstagio === 'agendado') return { bg: '#FEF3C7', text: '#D97706', label: 'AGENDADO' };
return { bg: themeStyles.verde + '20', text: themeStyles.verde, label: 'A DECORRER' };
return { bg: themeStyles.verde + '20', text: themeStyles.verde, label: 'EM CURSO' };
};
const badgeObj = getBadgeStyle();
const badgeObj = getBadgeStyle();
const renderAvisoEstadoDia = () => {
const reg = registosDiarios[selectedDate];
// Configuração base (Azul - Sem nada)
let config = {
icon: 'information-circle',
cor: themeStyles.azul,
bg: themeStyles.azulSuave,
texto: 'Sem Registo (Sem Nada)'
};
let config = { icon: 'information-circle', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Sem Registo de Atividade' };
if (!reg) {
// Se o dia não for válido para estágio ou for no futuro, não mostra nada
if (infoData.foraDeRange || !infoData.valida || selectedDate > hojeStr) return null;
} else if (reg.estado === 'presente') {
const temSumario = reg.sumario && reg.sumario.trim() !== '';
if (reg.estado_tutor === 'aprovado' && temSumario) {
config = { icon: 'checkmark-circle', cor: themeStyles.verde, bg: themeStyles.verde + '20', texto: 'Presença Confirmada e com Sumário' };
} else if (!temSumario) {
config = { icon: 'warning', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Presença Sem Sumário' };
} else {
config = { icon: 'time', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Presença Pendente de Aprovação' };
}
if (reg.estado_tutor === 'aprovado' && temSumario) config = { icon: 'checkmark-circle', cor: themeStyles.verde, bg: themeStyles.verde + '20', texto: 'Presença Confirmada e Validada' };
else if (!temSumario) config = { icon: 'warning', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Presença Requer Submissão de Atividades' };
else config = { icon: 'time', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Presença Pendente de Validação' };
} else {
if (reg.estado_tutor === 'aprovado') {
config = { icon: 'checkmark-done-circle', cor: themeStyles.cinzento, bg: themeStyles.cinzento + '20', texto: 'Falta Confirmada pela Entidade' };
} else if (reg.justificacao_url) {
config = { icon: 'document-text', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Falta Justificada (Em Análise)' };
} else {
config = { icon: 'close-circle', cor: themeStyles.vermelho, bg: themeStyles.vermelhoSuave, texto: 'Falta Injustificada' };
}
if (reg.estado_tutor === 'aprovado') config = { icon: 'checkmark-done-circle', cor: themeStyles.cinzento, bg: themeStyles.cinzento + '20', texto: 'Falta Confirmada pela Entidade' };
else if (reg.justificacao_url) config = { icon: 'document-text', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Documento em Análise pela Entidade' };
else config = { icon: 'close-circle', cor: themeStyles.vermelho, bg: themeStyles.vermelhoSuave, texto: 'Falta Injustificada' };
}
return (
@@ -430,10 +364,6 @@ const badgeObj = getBadgeStyle();
);
};
const horasTotais = Number(estagioDetalhes?.horas_totais) || 0;
const horasConcluidas = Number(estagioDetalhes?.horas_concluidas) || 0;
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
const regSelecionado = registosDiarios[selectedDate];
return (
@@ -447,12 +377,12 @@ const badgeObj = getBadgeStyle();
<View style={[styles.iconCircle, { backgroundColor: themeStyles.azulSuave }]}>
<Ionicons name="location" size={40} color={themeStyles.azul} />
</View>
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Confirmar Local</Text>
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Verificação Geográfica</Text>
<Text style={[styles.modalDesc, { color: themeStyles.textoSecundario }]}>
Precisamos de validar a tua localização para confirmar que estás no estágio.
Para registar a presença, é necessário validar as coordenadas geográficas da entidade de acolhimento.
</Text>
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: themeStyles.azul }]} onPress={executarMarcacao}>
<Text style={styles.txtBtn}>Confirmar e Marcar</Text>
<Text style={styles.txtBtn}>Autorizar e Registar</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnFechar} onPress={() => setShowLocationModal(false)}>
<Text style={[styles.txtFechar, { color: themeStyles.textoSecundario }]}>Cancelar</Text>
@@ -467,175 +397,154 @@ const badgeObj = getBadgeStyle();
</Animated.View>
)}
<ScrollView
ref={scrollViewRef}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />}
>
<ScrollView ref={scrollViewRef} contentContainerStyle={styles.container} showsVerticalScrollIndicator={false} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />}>
<View style={styles.topBar}>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
<View style={styles.topIcons}>
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')} style={{ marginRight: 15 }}>
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
</TouchableOpacity>
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}>
<Ionicons name="person-circle-outline" size={30} color={themeStyles.texto} />
</TouchableOpacity>
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')} style={{ marginRight: 15 }}><Ionicons name="settings-outline" size={26} color={themeStyles.texto} /></TouchableOpacity>
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}><Ionicons name="person-circle-outline" size={30} color={themeStyles.texto} /></TouchableOpacity>
</View>
</View>
<View style={[styles.quickActionsContainer, { backgroundColor: isDarkMode ? '#1E1E1E' : '#F1F5F9' }]}>
<TouchableOpacity
style={[styles.quickActionBtn, activeTab === 'horas' && styles.quickActionBtnActive, activeTab === 'horas' && { backgroundColor: themeStyles.card }]}
onPress={() => setActiveTab('horas')} activeOpacity={0.7}
>
<Ionicons name={activeTab === 'horas' ? "pie-chart" : "pie-chart-outline"} size={18} color={activeTab === 'horas' ? themeStyles.azul : themeStyles.textoSecundario} />
<Text style={[styles.quickActionText, { color: activeTab === 'horas' ? themeStyles.azul : themeStyles.textoSecundario }]}>Horas</Text>
<TouchableOpacity style={[styles.quickActionBtn, activeTab === 'assiduidade' && styles.quickActionBtnActive, activeTab === 'assiduidade' && { backgroundColor: themeStyles.card }]} onPress={() => setActiveTab('assiduidade')} activeOpacity={0.7}>
<Ionicons name={activeTab === 'assiduidade' ? "calendar" : "calendar-outline"} size={22} color={activeTab === 'assiduidade' ? themeStyles.azul : themeStyles.textoSecundario} />
<Text style={[styles.quickActionText, { color: activeTab === 'assiduidade' ? themeStyles.azul : themeStyles.textoSecundario }]}>Assiduidade</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.quickActionBtn, activeTab === 'horario' && styles.quickActionBtnActive, activeTab === 'horario' && { backgroundColor: themeStyles.card }]}
onPress={() => setActiveTab('horario')} activeOpacity={0.7}
>
<Ionicons name={activeTab === 'horario' ? "time" : "time-outline"} size={18} color={activeTab === 'horario' ? themeStyles.laranja : themeStyles.textoSecundario} />
<TouchableOpacity style={[styles.quickActionBtn, activeTab === 'horario' && styles.quickActionBtnActive, activeTab === 'horario' && { backgroundColor: themeStyles.card }]} onPress={() => setActiveTab('horario')} activeOpacity={0.7}>
<Ionicons name={activeTab === 'horario' ? "time" : "time-outline"} size={22} color={activeTab === 'horario' ? themeStyles.laranja : themeStyles.textoSecundario} />
<Text style={[styles.quickActionText, { color: activeTab === 'horario' ? themeStyles.laranja : themeStyles.textoSecundario }]}>Horário</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.quickActionBtn, activeTab === 'info' && styles.quickActionBtnActive, activeTab === 'info' && { backgroundColor: themeStyles.card }]}
onPress={() => setActiveTab('info')} activeOpacity={0.7}
>
<Ionicons name={activeTab === 'info' ? "information-circle" : "information-circle-outline"} size={18} color={activeTab === 'info' ? themeStyles.verde : themeStyles.textoSecundario} />
<Text style={[styles.quickActionText, { color: activeTab === 'info' ? themeStyles.verde : themeStyles.textoSecundario }]}>Info</Text>
<TouchableOpacity style={[styles.quickActionBtn, activeTab === 'info' && styles.quickActionBtnActive, activeTab === 'info' && { backgroundColor: themeStyles.card }]} onPress={() => setActiveTab('info')} activeOpacity={0.7}>
<Ionicons name={activeTab === 'info' ? "information-circle" : "information-circle-outline"} size={22} color={activeTab === 'info' ? themeStyles.verde : themeStyles.textoSecundario} />
<Text style={[styles.quickActionText, { color: activeTab === 'info' ? themeStyles.verde : themeStyles.textoSecundario }]}>Detalhes</Text>
</TouchableOpacity>
</View>
{!isLoadingDB && (
estagioDetalhes ? (
<View style={[styles.dashboardCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul }]}>
// ========================================================
// CAIXA MESTRA DO CONTEÚDO DA TAB
// Adicionado minHeight: 160 e justifyContent para fixar tamanho!
// ========================================================
<View style={[styles.dashboardCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: userRole !== 'aluno' ? (statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul) : undefined }]}>
<View style={styles.dashHeader}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: 8 }}>
<Ionicons name="business" size={20} color={statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul} />
<Text style={[styles.dashEmpresa, { color: themeStyles.texto }]} numberOfLines={1}>{estagioDetalhes.empresas?.nome || "Empresa não definida"}</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: badgeObj.bg }]}>
<Text style={[styles.statusBadgeText, { color: badgeObj.text }]}>{badgeObj.label}</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginTop: 4 }]} />
{activeTab === 'horas' && (
<View>
{/* LINHA DE CIMA: CÁLCULO DAS HORAS */}
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>REALIZADAS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{horasConcluidas}h</Text>
{/* O Cabeçalho (Empresa) só aparece para Professores e Entidades */}
{userRole !== 'aluno' && (
<>
<View style={styles.dashHeader}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: 8 }}>
<Ionicons name="business" size={20} color={statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul} />
<Text style={[styles.dashEmpresa, { color: themeStyles.texto }]} numberOfLines={1}>{estagioDetalhes.empresas?.nome || "Entidade Não Atribuída"}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>EM FALTA</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.laranja }]}>{horasEmFalta}h</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>TOTAIS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.texto }]}>{horasTotais}h</Text>
<View style={[styles.statusBadge, { backgroundColor: badgeObj.bg }]}>
<Text style={[styles.statusBadgeText, { color: badgeObj.text }]}>{badgeObj.label}</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
{/* LINHA DE BAIXO: ESTATÍSTICA DE PRESENÇAS E FALTAS */}
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
{/* Corrigido para PRESENÇAS */}
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>PRESENÇAS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{statsFaltas.totalPresencas}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>FALTAS JUST.</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.verde }]}>{statsFaltas.justificadas}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>FALTAS INJ.</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.vermelho }]}>{statsFaltas.injustificadas}</Text>
</View>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginTop: 4, marginBottom: 15 }]} />
</>
)}
{activeTab === 'horario' && (
<View style={{ alignItems: 'center', paddingVertical: 10 }}>
<Ionicons name="time" size={40} color={themeStyles.laranja} style={{ marginBottom: 10 }} />
<Text style={{ fontSize: 13, color: themeStyles.textoSecundario, fontWeight: '700', textTransform: 'uppercase' }}>Carga Horária</Text>
<Text style={{ fontSize: 24, fontWeight: '900', color: themeStyles.texto, marginTop: 2 }}>
{estagioDetalhes.horas_diarias ? estagioDetalhes.horas_diarias + '/dia' : 'Não definido'}
</Text>
{/* CONTEÚDO DAS TABS COM ALTURA FIXA */}
<View style={{ minHeight: 110, justifyContent: 'center' }}>
{/* ABA: ASSIDUIDADE */}
{activeTab === 'assiduidade' && (
<View>
{userRole === 'aluno' && (
<Text style={{ fontSize: 13, fontWeight: '900', color: themeStyles.textoSecundario, marginBottom: 15, textTransform: 'uppercase', textAlign: 'center', letterSpacing: 1 }}>
As Minhas Estatísticas
</Text>
)}
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>PRESENÇAS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{statsFaltas.totalPresencas}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>FALTAS JUST.</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.verde }]}>{statsFaltas.justificadas}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>FALTAS INJ.</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.vermelho }]}>{statsFaltas.injustificadas}</Text>
</View>
</View>
</View>
)}
{horariosEstagio.length > 0 && (
<View style={{ width: '100%', marginTop: 20 }}>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginVertical: 8 }]} />
{horariosEstagio.map((h, index) => (
<View key={index} style={{ flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 8, paddingHorizontal: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<Ionicons name={h.periodo === 'Manhã' ? "partly-sunny-outline" : "sunny-outline"} size={16} color={themeStyles.textoSecundario} />
<Text style={{ fontSize: 15, fontWeight: '800', color: themeStyles.textoSecundario }}>{h.periodo}</Text>
{/* ABA: HORÁRIO */}
{activeTab === 'horario' && (
<View style={{ alignItems: 'center' }}>
<Ionicons name="time" size={32} color={themeStyles.laranja} style={{ marginBottom: 5 }} />
<Text style={{ fontSize: 12, color: themeStyles.textoSecundario, fontWeight: '700', textTransform: 'uppercase' }}>Carga Horária Definida</Text>
<Text style={{ fontSize: 20, fontWeight: '900', color: themeStyles.texto, marginTop: 2 }}>
{estagioDetalhes.horas_diarias ? estagioDetalhes.horas_diarias + ' Horas/Dia' : 'Não definido'}
</Text>
{horariosEstagio.length > 0 && (
<View style={{ width: '100%', marginTop: 15 }}>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginVertical: 8 }]} />
{horariosEstagio.map((h, index) => (
<View key={index} style={{ flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4, paddingHorizontal: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<Ionicons name={h.periodo === 'Manhã' ? "partly-sunny-outline" : "sunny-outline"} size={14} color={themeStyles.textoSecundario} />
<Text style={{ fontSize: 13, fontWeight: '800', color: themeStyles.textoSecundario }}>{h.periodo}</Text>
</View>
<Text style={{ fontSize: 14, fontWeight: '900', color: themeStyles.texto }}>
{h.hora_inicio?.slice(0, 5)} - {h.hora_fim?.slice(0, 5)}
</Text>
</View>
<Text style={{ fontSize: 15, fontWeight: '900', color: themeStyles.texto }}>
{h.hora_inicio?.slice(0, 5)} - {h.hora_fim?.slice(0, 5)}
</Text>
</View>
))}
</View>
)}
</View>
)}
))}
</View>
)}
</View>
)}
{activeTab === 'info' && (
<View>
<View style={styles.infoRow}>
<Ionicons name="person" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Tutor da Empresa</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_nome || "N/A"}</Text>
{/* ABA: DETALHES DA ENTIDADE */}
{activeTab === 'info' && (
<View>
<View style={styles.infoRow}>
<Ionicons name="person" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Tutor da Entidade</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_nome || "N/A"}</Text>
</View>
</View>
<View style={styles.infoRow}>
<Ionicons name="call" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Contacto Oficial</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_telefone || "N/A"}</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Início do Processo</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_inicio}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Término Previsto</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_fim}</Text>
</View>
</View>
</View>
<View style={styles.infoRow}>
<Ionicons name="call" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Contacto</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_telefone || "N/A"}</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Data de Início</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_inicio}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Data de Fim (Prevista)</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_fim}</Text>
</View>
</View>
</View>
)}
)}
</View>
</View>
) : (
<View style={[styles.avisoBox, { backgroundColor: themeStyles.aviso }]}>
<Ionicons name="information-circle" size={24} color={themeStyles.avisoTexto} />
<Text style={[styles.avisoTexto, { color: themeStyles.avisoTexto }]}>
Sem estágio atribuído no sistema. Aguarda indicação do teu professor.
Processo de estágio ainda não atribuído. Aguarda notificação da coordenação.
</Text>
</View>
)
@@ -647,14 +556,14 @@ const badgeObj = getBadgeStyle();
onPress={handlePresencaClick}
disabled={!infoData.podeMarcar || isDiaMarcado() || isLocating}
>
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Registar Presença</Text>}
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, { backgroundColor: themeStyles.vermelho }, (!infoData.valida || isDiaMarcado()) && styles.disabled]}
onPress={handleFalta}
disabled={!infoData.valida || isDiaMarcado()}
>
<Text style={styles.txtBtn}>Marcar Falta</Text>
<Text style={styles.txtBtn}>Declarar Falta</Text>
</TouchableOpacity>
</View>
@@ -678,44 +587,41 @@ const badgeObj = getBadgeStyle();
<Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.textoSecundario }}>🎉 {infoData.nomeFeriado}</Text>
)}
{/* 🟢 MOSTRA O AVISO DO ESTADO DO TUTOR */}
{renderAvisoEstadoDia()}
{/* SE O ALUNO ESTIVER PRESENTE, MOSTRA O SUMÁRIO */}
{regSelecionado?.estado === 'presente' && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.rowTitle}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Relatório de Atividades</Text>
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={themeStyles.azul} /></TouchableOpacity>
</View>
<TextInput
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto, backgroundColor: themeStyles.fundo }]}
multiline editable={editandoSumario} value={sumarioInput}
onChangeText={setSumarioInput}
placeholder="O que fizeste hoje?" placeholderTextColor="#94A3B8"
placeholder="Descreve as tarefas realizadas e os conhecimentos aplicados..." placeholderTextColor="#94A3B8"
/>
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Submeter para Validação</Text></TouchableOpacity>}
</View>
)}
{/* SE O ALUNO FALTOU, MOSTRA O UPLOAD DE JUSTIFICAÇÃO */}
{regSelecionado?.estado === 'faltou' && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar Falta</Text>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Documento de Justificação</Text>
{regSelecionado.justificacao_url ? (
<View style={styles.justificadoBox}>
<Ionicons name="checkmark-circle" size={20} color={themeStyles.verde} />
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Justificativo Enviado à Entidade</Text>
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Documento Submetido à Entidade</Text>
</View>
) : (
<>
<TouchableOpacity style={[styles.btnUpload, { borderColor: themeStyles.azul }]} onPress={selecionarDocumento}>
<Ionicons name="document-attach-outline" size={20} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '600' }}>{pdf ? pdf.name : "Selecionar Documento PDF"}</Text>
<Text style={{ color: themeStyles.azul, fontWeight: '600' }}>{pdf ? pdf.name : "Anexar Documento PDF"}</Text>
</TouchableOpacity>
{pdf && (
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.azul }]} onPress={enviarJustificativo} disabled={isUploading}>
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Documento</Text>}
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Anexo</Text>}
</TouchableOpacity>
)}
</>
@@ -734,10 +640,10 @@ const styles = StyleSheet.create({
topIcons: { flexDirection: 'row', alignItems: 'center' },
title: { fontSize: 26, fontWeight: '900' },
quickActionsContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20, borderRadius: 20, padding: 6 },
quickActionBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, borderRadius: 16, gap: 6, elevation: 0, shadowOpacity: 0, borderWidth: 0 },
quickActionsContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20, borderRadius: 20, padding: 6, gap: 5 },
quickActionBtn: { flex: 1, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, borderRadius: 16, gap: 4, elevation: 0, shadowOpacity: 0, borderWidth: 0 },
quickActionBtnActive: { elevation: 0, shadowOpacity: 0, borderWidth: 0 },
quickActionText: { fontSize: 13, fontWeight: '800' },
quickActionText: { fontSize: 11, fontWeight: '800', textAlign: 'center' },
dashboardCard: { padding: 18, borderRadius: 20, borderWidth: 1, borderLeftWidth: 5, marginBottom: 20, elevation: 2, shadowOpacity: 0.05, shadowRadius: 8 },
dashHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 },

View File

@@ -8,16 +8,16 @@ import * as Sharing from 'expo-sharing';
import * as WebBrowser from 'expo-web-browser';
import { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View
ActivityIndicator,
Alert,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../../../lib/supabase';
import { useTheme } from '../../../themecontext';
@@ -361,7 +361,7 @@ export default function GestaoRelatorios() {
{/* 3. DIÁRIO DE BORDO (PDF) - AGORA COM O ALUNO_ID! */}
<View style={[styles.moduloBox, { borderBottomWidth: 0, paddingBottom: 0, marginBottom: 0 }]}>
<View style={styles.moduloHeader}>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>3. Diário de Bordo</Text>
<Text style={[styles.moduloTitle, { color: cores.texto }]}>3. Registos Diários</Text>
<Text style={[styles.notaTag, { backgroundColor: cores.verdeAgua + '30', color: '#003049' }]}>{r.horas_concluidas}h Registadas</Text>
</View>
<TouchableOpacity