This commit is contained in:
2026-05-04 23:46:26 +01:00
parent 0a57a3d8ba
commit da338b4ac4
3 changed files with 604 additions and 375 deletions

View File

@@ -57,18 +57,14 @@ const AlunoHome = memo(() => {
const [estagioDetalhes, setEstagioDetalhes] = useState<any>(null);
const [horariosEstagio, setHorariosEstagio] = useState<any[]>([]);
// 🟢 NOVOS ESTADOS PARA REFLETIR A DECISÃO DO TUTOR
const [presencasPendentes, setPresencasPendentes] = useState<Record<string, boolean>>({});
const [presencasAprovadas, setPresencasAprovadas] = useState<Record<string, boolean>>({});
const [presencasRejeitadas, setPresencasRejeitadas] = useState<Record<string, boolean>>({});
const [faltas, setFaltas] = useState<Record<string, boolean>>({});
const [sumarios, setSumarios] = useState<Record<string, string>>({});
const [urlsJustificacao, setUrlsJustificacao] = useState<Record<string, string>>({});
// 🟢 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 });
const [pdf, setPdf] = useState<any>(null);
const [editandoSumario, setEditandoSumario] = useState(false);
const [sumarioInput, setSumarioInput] = useState("");
const [isLoadingDB, setIsLoadingDB] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [isLocating, setIsLocating] = useState(false);
@@ -88,10 +84,12 @@ const AlunoHome = memo(() => {
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: azulPetroleo,
laranja: laranjaEPVC,
amarelo: '#F59E0B', // Adicionado para as faltas por confirmar
cinzento: '#94A3B8', // Adicionado para presenças por confirmar
verde: '#10B981',
vermelho: '#EF4444',
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
vermelho: '#EF4444',
verde: '#10B981',
aviso: isDarkMode ? '#2D2200' : '#FFF9E6',
avisoTexto: isDarkMode ? '#FFD700' : '#856404'
}), [isDarkMode]);
@@ -142,43 +140,32 @@ const AlunoHome = memo(() => {
if (error) throw error;
// 🟢 SEPARAMOS AS PRESENÇAS PELO ESTADO DO TUTOR
const pPendente: any = {}, pAprovada: any = {}, pRejeitada: any = {};
const f: any = {}, s: any = {}, u: any = {};
let countJustificadas = 0;
let countInjustificadas = 0;
let countPresencasAprovadas = 0;
const novosRegistos: Record<string, any> = {};
let countJustificadas = 0, countInjustificadas = 0, countPresencasAprovadas = 0;
data?.forEach(item => {
if (item.estado === 'presente') {
// Guarda o estado para o calendário
if (item.estado_tutor === 'pendente') pPendente[item.data] = true;
else if (item.estado_tutor === 'aprovado') {
pAprovada[item.data] = true;
countPresencasAprovadas++; // Só soma se o tutor aprovou
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++;
}
}
else if (item.estado_tutor === 'rejeitado') pRejeitada[item.data] = true;
s[item.data] = item.sumario || '';
} else {
f[item.data] = true;
u[item.data] = item.justificacao_url || '';
if (item.justificacao_url) countJustificadas++;
else countInjustificadas++;
}
});
setPresencasPendentes(pPendente);
setPresencasAprovadas(pAprovada);
setPresencasRejeitadas(pRejeitada);
setFaltas(f);
setSumarios(s);
setUrlsJustificacao(u);
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("");
} else {
setPresencasPendentes({}); setPresencasAprovadas({}); setPresencasRejeitadas({});
setFaltas({}); setSumarios({}); setUrlsJustificacao({});
setRegistosDiarios({});
setStatsFaltas({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
}
@@ -189,7 +176,7 @@ const AlunoHome = memo(() => {
}
};
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, []));
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, [selectedDate]));
useEffect(() => {
const estagiosSubscription = supabase
@@ -212,7 +199,7 @@ const AlunoHome = memo(() => {
setRefreshing(true);
await fetchDadosSupabase(true);
setRefreshing(false);
}, []);
}, [selectedDate]);
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
@@ -241,9 +228,31 @@ const AlunoHome = memo(() => {
};
}, [selectedDate, estagioDetalhes, hojeStr, feriadosMap, statusEstagio]);
// 🟢 FUNÇÃO AUXILIAR PARA SABER SE O DIA JÁ TEM REGISTO (PARA DESATIVAR BOTÕES)
const isDiaMarcado = () => {
return !!presencasAprovadas[selectedDate] || !!presencasPendentes[selectedDate] || !!presencasRejeitadas[selectedDate] || !!faltas[selectedDate];
// Modificado para ver no "cofre" em vez das antigas variáveis
const isDiaMarcado = () => !!registosDiarios[selectedDate];
const savePresencaData = async (payload: any, successMessage: string) => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Usuário não autenticado.");
const finalPayload = { ...payload, aluno_id: user.id, data: selectedDate };
const existing = registosDiarios[selectedDate];
if (existing) {
const { error } = await supabase.from('presencas').update(finalPayload).eq('id', existing.id);
if (error) throw error;
} else {
const { error } = await supabase.from('presencas').insert([finalPayload]);
if (error) throw error;
}
await fetchDadosSupabase(true);
showAlert(successMessage, "success");
} catch (e: any) {
console.error(e);
showAlert(e.message || "Erro ao guardar registo.", "error");
}
};
const handlePresencaClick = async () => {
@@ -265,28 +274,29 @@ const AlunoHome = memo(() => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') throw new Error("Sem acesso ao GPS.");
const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('presencas').upsert({
aluno_id: user?.id,
data: selectedDate,
estado: 'presente',
lat: loc.coords.latitude,
await savePresencaData({
estado: 'presente',
lat: loc.coords.latitude,
lng: loc.coords.longitude,
estado_tutor: 'pendente' // 🟢 NOVO REGISTO ENTRA COMO PENDENTE
});
showAlert("Presença marcada! A aguardar aprovação.", "success");
} catch (e: any) { showAlert(e.message, "error"); }
finally { setIsLocating(false); }
estado_tutor: 'pendente'
}, "Presença marcada! A aguardar aprovação da empresa.");
} catch (e: any) {
showAlert(e.message, "error");
} finally {
setIsLocating(false);
}
};
const handleFalta = async () => {
if (infoData.foraDeRange) return showAlert("Data fora do período de estágio.", "error");
if (!infoData.valida) return showAlert("Não é possível registar falta hoje.", "error");
try {
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' });
showAlert("Falta registada.", "info");
} catch (e) { showAlert("Erro ao registar falta.", "error"); }
await savePresencaData({
estado: 'faltou',
estado_tutor: 'pendente'
}, "Falta registada e enviada para a entidade.");
};
const selecionarDocumento = async () => {
@@ -303,21 +313,72 @@ const AlunoHome = memo(() => {
const fileName = `${user?.id}/${selectedDate}_justificacao.pdf`;
const { error: uploadError } = await supabase.storage.from('justificacoes').upload(fileName, decode(fileBase64), { contentType: 'application/pdf', upsert: true });
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
await savePresencaData({
justificacao_url: publicUrl,
estado_tutor: 'pendente'
}, "Justificativo enviado à entidade com sucesso!");
setPdf(null);
showAlert("Enviado com sucesso!", "success");
} catch (e) { showAlert("Erro no upload.", "error"); }
finally { setIsUploading(false); }
} catch (e) {
showAlert("Erro no upload do documento.", "error");
} finally {
setIsUploading(false);
}
};
const guardarSumario = async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('presencas').update({ sumario: sumarios[selectedDate] }).match({ aluno_id: user?.id, data: selectedDate });
setEditandoSumario(false);
showAlert("Sumário guardado!", "success");
} catch (e) { showAlert("Erro ao guardar.", "error"); }
await savePresencaData({
sumario: sumarioInput,
estado_tutor: 'pendente'
}, "Sumário submetido 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
}
}
}
}
// 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)
} 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
}
marcacoes[reg.data] = { marked: true, dotColor: cor };
});
marcacoes[selectedDate] = { ...marcacoes[selectedDate], selected: true, selectedColor: themeStyles.azul };
return marcacoes;
};
const getBadgeStyle = () => {
@@ -325,26 +386,70 @@ const AlunoHome = memo(() => {
if (statusEstagio === 'agendado') return { bg: '#FEF3C7', text: '#D97706', label: 'AGENDADO' };
return { bg: themeStyles.verde + '20', text: themeStyles.verde, label: 'A DECORRER' };
};
const badgeObj = getBadgeStyle();
const badgeObj = getBadgeStyle();
const horasTotais = estagioDetalhes?.horas_totais || 0;
const horasConcluidas = estagioDetalhes?.horas_concluidas || 0;
const horasPorDia = Number(estagioDetalhes?.horas_diarias || 0);
const horasConcluidas = statsFaltas.totalPresencas * horasPorDia;
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
// 🟢 FUNÇÃO PARA MOSTRAR AVISO DE ESTADO DO DIA
const renderAvisoEstadoDia = () => {
if (presencasAprovadas[selectedDate]) {
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.verde }}> Horas validadas pela empresa</Text>;
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)'
};
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;
// Se for um dia válido que já passou e está vazio, usa a config base (Azul)
} 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' };
}
} 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 (presencasPendentes[selectedDate]) {
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.laranja }}> A aguardar validação do tutor</Text>;
}
if (presencasRejeitadas[selectedDate]) {
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.vermelho }}> O tutor rejeitou este registo. Corrige o sumário.</Text>;
}
return null;
return (
<View style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: config.bg,
padding: 16,
borderRadius: 16,
marginTop: 15,
borderWidth: 1,
borderColor: config.cor + '40' // Adiciona um bocado de transparência à borda
}}>
<Ionicons name={config.icon as any} size={28} color={config.cor} />
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: config.cor }}>
{config.texto}
</Text>
</View>
);
};
const regSelecionado = registosDiarios[selectedDate];
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
@@ -353,14 +458,14 @@ const AlunoHome = memo(() => {
<View style={styles.modalOverlay}>
<View style={[styles.locationModal, { backgroundColor: themeStyles.card }]}>
<View style={styles.modalHandle} />
<View style={[styles.iconCircle, { backgroundColor: azulPetroleo + '15' }]}>
<Ionicons name="location" size={40} color={azulPetroleo} />
<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.modalDesc, { color: themeStyles.textoSecundario }]}>
Precisamos de validar a tua localização para confirmar que estás no estágio.
</Text>
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: azulPetroleo }]} onPress={executarMarcacao}>
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: themeStyles.azul }]} onPress={executarMarcacao}>
<Text style={styles.txtBtn}>Confirmar e Marcar</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnFechar} onPress={() => setShowLocationModal(false)}>
@@ -371,7 +476,7 @@ const AlunoHome = memo(() => {
</Modal>
{alertConfig && (
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? '#EF4444' : azulPetroleo }]}>
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? themeStyles.vermelho : themeStyles.azul }]}>
<Text style={styles.alertText}>{alertConfig.msg}</Text>
</Animated.View>
)}
@@ -380,9 +485,7 @@ const AlunoHome = memo(() => {
ref={scrollViewRef}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />
}
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>
@@ -396,80 +499,40 @@ const AlunoHome = memo(() => {
</View>
</View>
{/* 🚀 BOTÕES DE SEPARADOR (TABS) MODERNOS 🚀 */}
<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}
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>
<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>
<TouchableOpacity
style={[
styles.quickActionBtn,
activeTab === 'horario' && styles.quickActionBtnActive,
activeTab === 'horario' && { backgroundColor: themeStyles.card }
]}
onPress={() => setActiveTab('horario')}
activeOpacity={0.7}
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}
/>
<Text style={[styles.quickActionText, { color: activeTab === 'horario' ? themeStyles.laranja : themeStyles.textoSecundario }]}>
Horário
</Text>
<Ionicons name={activeTab === 'horario' ? "time" : "time-outline"} size={18} 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}
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>
<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>
</View>
{/* CARTÃO PRINCIPAL QUE MUDA CONFORME O SEPARADOR ATIVO */}
{!isLoadingDB && (
estagioDetalhes ? (
<View style={[styles.dashboardCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul }]}>
{/* O Cabeçalho (Empresa e Status) fica sempre visível */}
<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>
<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>
@@ -478,17 +541,17 @@ const AlunoHome = memo(() => {
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginTop: 4 }]} />
{/* CONTEÚDO 1: HORAS */}
{activeTab === 'horas' && (
<View>
<View style={styles.dashGrid}>
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>REALIZADAS</Text>
{/* 🟢 AGORA USA A VARIÁVEL CALCULADA */}
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{horasConcluidas}h</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>EM FALTA</Text>
{/* 🟢 AGORA USA A VARIÁVEL CALCULADA */}
<Text style={[styles.dashStatValue, { color: themeStyles.laranja }]}>{horasEmFalta}h</Text>
</View>
<View style={styles.dashDividerVertical} />
@@ -496,8 +559,7 @@ const AlunoHome = memo(() => {
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>TOTAIS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.texto }]}>{horasTotais}h</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<View style={styles.dashGrid}>
@@ -519,7 +581,6 @@ const AlunoHome = memo(() => {
</View>
)}
{/* CONTEÚDO 2: HORÁRIO DIÁRIO */}
{activeTab === 'horario' && (
<View style={{ alignItems: 'center', paddingVertical: 10 }}>
<Ionicons name="time" size={40} color={themeStyles.laranja} style={{ marginBottom: 10 }} />
@@ -528,11 +589,9 @@ const AlunoHome = memo(() => {
{estagioDetalhes.horas_diarias ? estagioDetalhes.horas_diarias + '/dia' : 'Não definido'}
</Text>
{/* Mostra a Manhã e a Tarde se existirem na base de dados */}
{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 }}>
@@ -549,10 +608,8 @@ const AlunoHome = memo(() => {
</View>
)}
{/* CONTEÚDO 3: INFO GERAL */}
{activeTab === 'info' && (
<View>
{/* Tutor e Contacto */}
<View style={styles.infoRow}>
<Ionicons name="person" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
@@ -569,7 +626,6 @@ const AlunoHome = memo(() => {
</View>
</View>
{/* Datas */}
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
@@ -598,7 +654,7 @@ const AlunoHome = memo(() => {
<View style={styles.botoesLinha}>
<TouchableOpacity
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || isDiaMarcado()) && styles.disabled]}
style={[styles.btn, { backgroundColor: themeStyles.laranja }, (!infoData.podeMarcar || isDiaMarcado()) && styles.disabled]}
onPress={handlePresencaClick}
disabled={!infoData.podeMarcar || isDiaMarcado() || isLocating}
>
@@ -618,18 +674,14 @@ const AlunoHome = memo(() => {
key={isDarkMode ? 'dark' : 'light'}
theme={{
calendarBackground: themeStyles.card, dayTextColor: themeStyles.texto, monthTextColor: themeStyles.texto,
todayTextColor: azulPetroleo, selectedDayBackgroundColor: azulPetroleo, textDisabledColor: isDarkMode ? '#333' : '#DDD'
todayTextColor: themeStyles.azul, selectedDayBackgroundColor: themeStyles.azul, textDisabledColor: isDarkMode ? '#333' : '#DDD'
}}
markedDates={{
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
// 🟢 AS CORES AGORA REFLETEM O ESTADO DO TUTOR
...Object.keys(presencasAprovadas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.verde } }), {}),
...Object.keys(presencasPendentes).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.laranja } }), {}),
...Object.keys(presencasRejeitadas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
markedDates={gerarMarcacoesCalendario()}
onDayPress={(day) => {
setSelectedDate(day.dateString);
setEditandoSumario(false);
setSumarioInput(registosDiarios[day.dateString]?.sumario || "");
}}
onDayPress={(day) => setSelectedDate(day.dateString)}
/>
</View>
@@ -640,40 +692,41 @@ const AlunoHome = memo(() => {
{/* 🟢 MOSTRA O AVISO DO ESTADO DO TUTOR */}
{renderAvisoEstadoDia()}
{/* SE O ALUNO ESTIVER PRESENTE (Pendente ou Aprovado), MOSTRA O SUMÁRIO */}
{(presencasPendentes[selectedDate] || presencasAprovadas[selectedDate] || presencasRejeitadas[selectedDate]) && (
{/* 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>
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>
<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 }]}
multiline editable={editandoSumario} value={sumarios[selectedDate]}
onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})}
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"
/>
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar Sumário</Text></TouchableOpacity>}
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Submeter para Validação</Text></TouchableOpacity>}
</View>
)}
{faltas[selectedDate] && (
{/* 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</Text>
{urlsJustificacao[selectedDate] ? (
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar Falta</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</Text>
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Justificativo Enviado à Entidade</Text>
</View>
) : (
<>
<TouchableOpacity style={[styles.btnUpload, { borderColor: azulPetroleo }]} onPress={selecionarDocumento}>
<Ionicons name="document-attach-outline" size={20} color={azulPetroleo} />
<Text style={{ color: azulPetroleo, fontWeight: '600' }}>{pdf ? pdf.name : "Selecionar PDF"}</Text>
<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>
</TouchableOpacity>
{pdf && (
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: azulPetroleo }]} onPress={enviarJustificativo} disabled={isUploading}>
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter</Text>}
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.azul }]} onPress={enviarJustificativo} disabled={isUploading}>
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Documento</Text>}
</TouchableOpacity>
)}
</>
@@ -719,6 +772,7 @@ const styles = StyleSheet.create({
alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' },
avisoBox: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 18, borderRadius: 18, marginBottom: 20 },
avisoTexto: { fontSize: 14, fontWeight: '700', flex: 1, lineHeight: 20 },
avisoTxt: { textAlign: 'center', marginTop: 15, fontWeight: '800', fontSize: 15 },
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
btn: { padding: 18, borderRadius: 22, width: '48%', alignItems: 'center', elevation: 3 },
txtBtn: { color: '#fff', fontWeight: '800', fontSize: 14 },
@@ -727,7 +781,7 @@ const styles = StyleSheet.create({
card: { padding: 20, borderRadius: 25, marginTop: 20, borderWidth: 1 },
cardTitulo: { fontSize: 18, fontWeight: '700' },
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 },
input: { borderWidth: 1, borderRadius: 15, padding: 15, height: 100, textAlignVertical: 'top' },
input: { borderWidth: 1, borderRadius: 15, padding: 15, height: 100, textAlignVertical: 'top', fontSize: 14, fontWeight: '600' },
btnSalvar: { padding: 15, borderRadius: 15, marginTop: 15, alignItems: 'center' },
btnUpload: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 15, borderRadius: 15, borderWidth: 2, borderStyle: 'dashed' },
justificadoBox: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 10 },

View File

@@ -1,56 +1,70 @@
// app/Empresa/EmpresaHome.tsx
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
ActivityIndicator,
Alert,
Animated,
Linking,
Modal,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { supabase } from '../../lib/supabase';
import { useTheme } from '../../themecontext';
// Tipos de Ecrã possíveis neste "Tudo-em-Um"
type AppScreen = 'DASHBOARD' | 'ALUNOS' | 'PEDIDOS_LISTA' | 'PEDIDOS_HISTORICO' | 'AVALIACOES' | 'DEFINICOES';
export default function EmpresaHome() {
const { isDarkMode } = useTheme();
const router = useRouter();
const [pendentes, setPendentes] = useState<any[]>([]);
// ESTADOS DE NAVEGAÇÃO E DADOS
const [currentScreen, setCurrentScreen] = useState<AppScreen>('DASHBOARD');
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [empresaNome, setEmpresaNome] = useState('');
const [empresaData, setEmpresaData] = useState<any>(null);
const [listaAlunos, setListaAlunos] = useState<any[]>([]);
const [presencasGerais, setPresencasGerais] = useState<any[]>([]);
// ESTADOS PARA MODAIS E SELEÇÕES
const [alunoSelecionado, setAlunoSelecionado] = useState<any>(null);
const [modalDetalhesAluno, setModalDetalhesAluno] = useState(false);
// Estados do Toast Animado
// 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,
azul: '#2390a6',
laranja: '#E38E00',
verde: '#10B981',
vermelho: '#EF4444',
azulSuave: '#00c3ff',
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
aviso: isDarkMode ? '#2D2200' : '#FFF9E6',
avisoTexto: isDarkMode ? '#FFD700' : '#856404'
}), [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(() => {
Animated.timing(slideAnim, { toValue: Platform.OS === 'ios' ? 50 : 20, duration: 300, useNativeDriver: true }).start(() => {
setTimeout(() => {
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
.start(() => setToast({ visible: false, message: '', type: 'info' }));
@@ -58,93 +72,82 @@ export default function EmpresaHome() {
});
}, [slideAnim]);
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
// 🚀 BUSCAR TUDO DE UMA VEZ
const fetchTudo = 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;
// 1. Empresa
const { data: empresa } = await supabase.from('empresas').select('*').eq('user_id', user.id).maybeSingle();
if (!empresa) {
setLoading(false);
return Alert.alert("Erro", "Conta não associada a nenhuma empresa.");
}
setEmpresaNome(empresa.nome);
setEmpresaData(empresa);
// 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;
}
// 2. Alunos
const { data: estagios } = await supabase.from('estagios').select('aluno_id').eq('empresa_id', empresa.id);
const alunoIds = estagios?.map(e => e.aluno_id) || [];
const alunoIds = estagios.map(e => e.aluno_id);
if (alunoIds.length > 0) {
const { data: alunos } = await supabase.from('alunos').select('*').in('id', alunoIds);
setListaAlunos(alunos || []);
// 3. Buscar nomes dos alunos
const { data: alunos } = await supabase
.from('alunos')
.select('id, nome')
.in('id', alunoIds);
// 3. Presenças e Faltas
const { data: presencas } = await supabase
.from('presencas')
.select('*')
.in('aluno_id', alunoIds)
.order('data', { ascending: false });
setPresencasGerais(presencas || []);
} else {
setListaAlunos([]);
setPresencasGerais([]);
}
const mapaAlunos: Record<string, string> = {};
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");
showToast("Erro ao carregar dados", "error");
} finally {
if (!isManualRefresh) setLoading(false);
setRefreshing(false);
}
};
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
useFocusEffect(useCallback(() => { fetchTudo(); }, []));
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchValidaçõesPendentes(true);
fetchTudo(true);
}, []);
// 🟢 APROVAR OU RECUSAR (COM VERIFICAÇÃO ANTI-MENTIRAS DO SUPABASE)
const lidarComPresenca = async (id: string, decisao: 'aprovado' | 'rejeitado') => {
try {
const { error } = await supabase
.from('presencas')
.update({ estado_tutor: decisao })
.eq('id', id);
if (decisao === 'rejeitado') {
// Tenta apagar e pede confirmação de volta (.select)
const { data, error } = await supabase.from('presencas').delete().eq('id', id).select();
if (error) throw error;
if (!data || data.length === 0) throw new Error("A Base de Dados bloqueou a ação (Verifica se desligaste o RLS no Supabase)!");
if (error) throw error;
showToast(decisao === 'aprovado' ? "Registo aprovado!" : "Registo rejeitado.", decisao === 'aprovado' ? 'success' : 'info');
setPendentes(prev => prev.filter(p => p.id !== id));
showToast("Registo recusado e apagado!", "info");
// Remove da lista local da empresa
setPresencasGerais(prev => prev.filter(p => p.id !== id));
} else {
// Tenta atualizar e pede confirmação de volta (.select)
const { data, error } = await supabase.from('presencas').update({ estado_tutor: decisao }).eq('id', id).select();
if (error) throw error;
if (!data || data.length === 0) throw new Error("A Base de Dados bloqueou a ação (Verifica se desligaste o RLS no Supabase)!");
showToast("Validado com sucesso!", "success");
// Atualiza a lista local da empresa
setPresencasGerais(prev => prev.map(p => p.id === id ? { ...p, estado_tutor: decisao } : p));
}
} catch (e: any) {
showToast("Erro ao processar validação.", "error");
Alert.alert("Erro a Validar", e.message || "Não foi possível alterar o registo na Base de Dados.");
}
};
@@ -154,11 +157,225 @@ export default function EmpresaHome() {
return parts.length !== 3 ? dataStr : `${parts[2]}/${parts[1]}/${parts[0]}`;
};
// ==========================================
// COMPONENTES DOS DIFERENTES ECRÃS (VIEWS)
// ==========================================
const renderDashboard = () => (
<View style={styles.grid}>
{/* CARD 1: ALUNOS */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('ALUNOS')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.azulSuave }]}>
<Ionicons name="people" size={32} color={themeStyles.azul} />
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Alunos</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Verificar estagiários e detalhes.</Text>
</TouchableOpacity>
{/* CARD 2: PEDIDOS */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('PEDIDOS_LISTA')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.laranja + '20' }]}>
<Ionicons name="documents" size={32} color={themeStyles.laranja} />
{presencasGerais.filter(p => p.estado_tutor === 'pendente').length > 0 && (
<View style={styles.badgeNotif} />
)}
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Pedidos</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Validar sumários e faltas.</Text>
</TouchableOpacity>
{/* CARD 3: AVALIAÇÕES */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('AVALIACOES')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.verde + '20' }]}>
<Ionicons name="star" size={32} color={themeStyles.verde} />
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Avaliações</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Em breve.</Text>
</TouchableOpacity>
{/* CARD 4: DEFINIÇÕES */}
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('DEFINICOES')}>
<View style={[styles.dashIcon, { backgroundColor: themeStyles.secundario + '20' }]}>
<Ionicons name="settings" size={32} color={themeStyles.secundario} />
</View>
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Definições</Text>
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Gerir conta da empresa.</Text>
</TouchableOpacity>
</View>
);
const renderAlunos = () => (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar ao Menu</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Estagiários ({listaAlunos.length})</Text>
{listaAlunos.length === 0 ? (
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 40 }}>Nenhum aluno associado a esta empresa.</Text>
) : (
listaAlunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
style={[styles.listCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => { setAlunoSelecionado(aluno); setModalDetalhesAluno(true); }}
>
<View style={[styles.avatar, { backgroundColor: themeStyles.azulSuave }]}>
<Text style={[styles.avatarText, { color: themeStyles.azul }]}>{aluno.nome.charAt(0)}</Text>
</View>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.listCardTitle, { color: themeStyles.texto }]}>{aluno.nome}</Text>
<Text style={[styles.listCardSub, { color: themeStyles.secundario }]}>Toque para ver detalhes</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={themeStyles.secundario} />
</TouchableOpacity>
))
)}
</View>
);
const renderPedidosLista = () => (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar ao Menu</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Validar Registos</Text>
<Text style={{ color: themeStyles.secundario, marginBottom: 20 }}>Selecione um aluno para ver o histórico e validar pedidos pendentes.</Text>
{listaAlunos.map(aluno => {
const pendentesDoAluno = presencasGerais.filter(p => p.aluno_id === aluno.id && p.estado_tutor === 'pendente').length;
return (
<TouchableOpacity
key={aluno.id}
style={[styles.listCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
onPress={() => { setAlunoSelecionado(aluno); setCurrentScreen('PEDIDOS_HISTORICO'); }}
>
<View style={{ flex: 1 }}>
<Text style={[styles.listCardTitle, { color: themeStyles.texto }]}>{aluno.nome}</Text>
</View>
{pendentesDoAluno > 0 ? (
<View style={[styles.badgeCount, { backgroundColor: themeStyles.vermelho }]}>
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 12 }}>{pendentesDoAluno} PENDENTES</Text>
</View>
) : (
<Ionicons name="checkmark-circle" size={24} color={themeStyles.verde} />
)}
</TouchableOpacity>
);
})}
</View>
);
const renderPedidosHistorico = () => {
if (!alunoSelecionado) return null;
const historicoAluno = presencasGerais.filter(p => p.aluno_id === alunoSelecionado.id);
return (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('PEDIDOS_LISTA')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar à Lista</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto, fontSize: 20 }]}>Histórico: {alunoSelecionado.nome}</Text>
{historicoAluno.length === 0 ? (
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 40 }}>Nenhum registo efetuado por este aluno.</Text>
) : (
historicoAluno.map(item => (
<View key={item.id} style={[styles.historyCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.historyTop}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>{formatarData(item.data)}</Text>
<View style={[styles.statusTag, { backgroundColor: item.estado === 'faltou' ? themeStyles.vermelho + '15' : themeStyles.verde + '15' }]}>
<Text style={[styles.statusTagText, { color: item.estado === 'faltou' ? themeStyles.vermelho : themeStyles.verde }]}>
{item.estado === 'faltou' ? 'FALTA' : 'PRESENÇA'}
</Text>
</View>
</View>
<View style={[styles.historyBody, { backgroundColor: themeStyles.fundo }]}>
<Text style={{ color: themeStyles.texto, fontWeight: '600' }}>
{item.estado === 'presente' ? (item.sumario || "Sem sumário registado.") : "Aluno marcou ausência."}
</Text>
{item.justificacao_url && (
<TouchableOpacity style={{ marginTop: 10, flexDirection: 'row', alignItems: 'center' }} onPress={() => Linking.openURL(item.justificacao_url)}>
<Ionicons name="document-attach" size={16} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', marginLeft: 5 }}>Ver Justificativo PDF</Text>
</TouchableOpacity>
)}
</View>
{/* SÓ MOSTRA BOTÕES SE ESTIVER PENDENTE */}
{item.estado_tutor === 'pendente' ? (
<View style={styles.actionRow}>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]} onPress={() => lidarComPresenca(item.id, 'rejeitado')}>
<Ionicons name="trash-outline" size={20} color={themeStyles.vermelho} />
<Text style={{ color: themeStyles.vermelho, fontWeight: '800', marginLeft: 5 }}>Recusar (Apagar)</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: themeStyles.verde }]} onPress={() => lidarComPresenca(item.id, 'aprovado')}>
<Ionicons name="checkmark" size={20} color="#fff" />
<Text style={{ color: '#fff', fontWeight: '800', marginLeft: 5 }}>Aprovar</Text>
</TouchableOpacity>
</View>
) : (
<Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '800', color: themeStyles.verde }}>
Aprovado
</Text>
)}
</View>
))
)}
</View>
);
};
const renderAvaliacoes = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<TouchableOpacity style={[styles.btnVoltar, { position: 'absolute', top: 0, left: 0 }]} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar</Text>
</TouchableOpacity>
<Ionicons name="construct" size={80} color={themeStyles.secundario} style={{ opacity: 0.3 }} />
<Text style={{ fontSize: 24, fontWeight: '900', color: themeStyles.texto, marginTop: 20 }}>BREVEMENTE</Text>
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 10 }}>A área de avaliações finais está em construção.</Text>
</View>
);
const renderDefinicoes = () => (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar</Text>
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Definições</Text>
<View style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, width: '100%', marginBottom: 20 }]}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800', fontSize: 12, textTransform: 'uppercase' }}>Nome da Empresa</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18, marginTop: 5 }}>{empresaData?.nome || 'N/A'}</Text>
<Text style={{ color: themeStyles.secundario, fontWeight: '800', fontSize: 12, textTransform: 'uppercase', marginTop: 15 }}>Tutor</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18, marginTop: 5 }}>{empresaData?.tutor_nome || 'N/A'}</Text>
</View>
<TouchableOpacity
style={{ backgroundColor: themeStyles.vermelho, padding: 18, borderRadius: 16, alignItems: 'center', flexDirection: 'row', justifyContent: 'center' }}
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
>
<Ionicons name="log-out" size={24} color="#fff" />
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 16, marginLeft: 10 }}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
);
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
{/* TOAST ANIMADO */}
<Animated.View style={[
styles.toastContainer,
{ transform: [{ translateY: slideAnim }] },
@@ -170,138 +387,96 @@ export default function EmpresaHome() {
<Text style={styles.toastText}>{toast.message}</Text>
</Animated.View>
<View style={styles.header}>
<View style={{ flex: 1 }}>
<Text style={[styles.greeting, { color: themeStyles.secundario }]}>Bem-vindo,</Text>
<Text style={[styles.title, { color: themeStyles.texto }]} numberOfLines={1}>
{empresaNome || 'Entidade'}
</Text>
</View>
<TouchableOpacity
style={[styles.logoutBtn, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
>
<Ionicons name="log-out-outline" size={22} color={themeStyles.vermelho} />
</TouchableOpacity>
</View>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
<View style={[styles.countBadge, { backgroundColor: themeStyles.laranja }]}>
<Text style={styles.countText}>{pendentes.length}</Text>
</View>
<View style={styles.headerArea}>
<Text style={[styles.appTitle, { color: themeStyles.texto }]}>Painel Empresa</Text>
{empresaData?.nome ? <Text style={{ color: themeStyles.secundario, fontWeight: '700' }}>{empresaData.nome}</Text> : null}
</View>
{loading && !refreshing ? (
<View style={styles.centerBox}>
<ActivityIndicator size="large" color={themeStyles.azul} />
</View>
<View style={styles.centerBox}><ActivityIndicator size="large" color={themeStyles.azul} /></View>
) : (
<ScrollView
contentContainerStyle={styles.scroll}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={themeStyles.azul} />}
>
{pendentes.length === 0 ? (
<View style={[styles.emptyBox, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={[styles.emptyIconCircle, { backgroundColor: themeStyles.azulSuave }]}>
<Ionicons name="shield-checkmark-outline" size={40} color={themeStyles.azul} />
</View>
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Tudo em ordem!</Text>
<Text style={[styles.emptyDesc, { color: themeStyles.secundario }]}>
Não existem registos pendentes de validação. **Vai dar merda** se os alunos não trabalharem! 😂
</Text>
</View>
) : (
pendentes.map((item) => (
<View key={item.id} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.cardTop}>
<View style={[styles.avatar, { backgroundColor: themeStyles.azulSuave }]}>
<Text style={[styles.avatarText, { color: themeStyles.azul }]}>{item.aluno_nome.charAt(0)}</Text>
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>{item.aluno_nome}</Text>
<View style={styles.dataRow}>
<Ionicons name="calendar-outline" size={14} color={themeStyles.laranja} />
<Text style={[styles.dataText, { color: themeStyles.secundario }]}>{formatarData(item.data)}</Text>
</View>
</View>
<View style={[styles.statusTag, { backgroundColor: themeStyles.laranja + '15' }]}>
<Text style={[styles.statusTagText, { color: themeStyles.laranja }]}>PENDENTE</Text>
</View>
</View>
<View style={[styles.sumarioBox, { backgroundColor: themeStyles.fundo }]}>
<Text style={[styles.sumarioLabel, { color: themeStyles.secundario }]}>SUMÁRIO DO DIA:</Text>
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
{item.sumario || "O aluno não descreveu as atividades deste dia."}
</Text>
</View>
<View style={styles.actionRow}>
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]}
onPress={() => lidarComPresenca(item.id, 'rejeitado')}
>
<Ionicons name="close-circle-outline" size={20} color={themeStyles.vermelho} />
<Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: themeStyles.verde }]}
onPress={() => lidarComPresenca(item.id, 'aprovado')}
>
<Ionicons name="checkmark-circle-outline" size={20} color="#fff" />
<Text style={[styles.btnActionText, { color: '#fff' }]}>Aprovar</Text>
</TouchableOpacity>
</View>
</View>
))
)}
{currentScreen === 'DASHBOARD' && renderDashboard()}
{currentScreen === 'ALUNOS' && renderAlunos()}
{currentScreen === 'PEDIDOS_LISTA' && renderPedidosLista()}
{currentScreen === 'PEDIDOS_HISTORICO' && renderPedidosHistorico()}
{currentScreen === 'AVALIACOES' && renderAvaliacoes()}
{currentScreen === 'DEFINICOES' && renderDefinicoes()}
</ScrollView>
)}
{/* MODAL DETALHES DO ALUNO */}
<Modal visible={modalDetalhesAluno} animationType="slide" transparent>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: themeStyles.card }]}>
<View style={styles.modalHandle} />
<Text style={[styles.pageTitle, { color: themeStyles.texto, textAlign: 'center' }]}>{alunoSelecionado?.nome}</Text>
<View style={[styles.infoBox, { backgroundColor: themeStyles.fundo }]}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}> ESCOLA</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18 }}>{alunoSelecionado?.n_escola || 'N/A'}</Text>
</View>
<View style={[styles.infoBox, { backgroundColor: themeStyles.fundo, marginTop: 10 }]}>
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>TURMA/CURSO</Text>
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18 }}>{alunoSelecionado?.turma_curso || 'N/A'}</Text>
</View>
<TouchableOpacity style={[styles.btnFecharModal, { backgroundColor: themeStyles.azul }]} onPress={() => setModalDetalhesAluno(false)}>
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 16 }}>Fechar</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
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 },
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6 },
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 },
headerArea: { paddingHorizontal: 20, paddingTop: 20, paddingBottom: 10 },
appTitle: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 },
scroll: { padding: 20, paddingBottom: 60 },
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 },
// DASHBOARD GRID
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
dashCard: { width: '48%', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
dashIcon: { width: 56, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
dashTitle: { fontSize: 18, fontWeight: '900', marginBottom: 5 },
dashDesc: { fontSize: 12, fontWeight: '600', lineHeight: 18 },
badgeNotif: { position: 'absolute', top: -5, right: -5, width: 14, height: 14, borderRadius: 7, backgroundColor: '#EF4444', borderWidth: 2, borderColor: '#fff' },
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 },
// SUB-PAGES
btnVoltar: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
pageTitle: { fontSize: 24, fontWeight: '900', marginBottom: 20 },
// LISTAS
listCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, borderWidth: 1, marginBottom: 12 },
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 },
listCardTitle: { fontSize: 16, fontWeight: '800' },
listCardSub: { fontSize: 12, fontWeight: '600', marginTop: 2 },
badgeCount: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12 },
// HISTÓRICO
historyCard: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
historyTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
statusTag: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
statusTagText: { fontSize: 10, fontWeight: '900', letterSpacing: 0.5 },
historyBody: { padding: 15, borderRadius: 16, marginBottom: 15 },
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 }
btnAction: { flex: 1, flexDirection: 'row', height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
// MODAL
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 30, alignItems: 'center', paddingBottom: 50 },
modalHandle: { width: 50, height: 6, backgroundColor: '#cbd5e1', borderRadius: 10, marginBottom: 20 },
infoBox: { width: '100%', padding: 15, borderRadius: 16, alignItems: 'center' },
btnFecharModal: { width: '100%', padding: 18, borderRadius: 16, alignItems: 'center', marginTop: 20 }
});

View File

@@ -51,7 +51,7 @@ export default function LoginScreen() {
} else if (data.tipo === 'aluno') {
router.replace('/Aluno/AlunoHome');
} else if (data.tipo === 'empresa') {
router.replace('/Empresas/EmpresaHome'); // 🟢 Rota da empresa adicionada!
router.replace('/Empresa/EmpresaHome'); // 🟢 Rota da empresa adicionada!
} else {
Alert.alert('Erro', 'Tipo de conta inválido');
}