From da338b4ac42b7188de9df88af1bc317eb6998baa Mon Sep 17 00:00:00 2001 From: Seu Nome <230413@epvc.pt> Date: Mon, 4 May 2026 23:46:26 +0100 Subject: [PATCH] ATUA --- app/Aluno/AlunoHome.tsx | 420 +++++++++++++++------------ app/Empresa/EmpresaHome.tsx | 557 +++++++++++++++++++++++------------- app/index.tsx | 2 +- 3 files changed, 604 insertions(+), 375 deletions(-) diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index 5804160..f2b63bd 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -57,18 +57,14 @@ const AlunoHome = memo(() => { const [estagioDetalhes, setEstagioDetalhes] = useState(null); const [horariosEstagio, setHorariosEstagio] = useState([]); - // 🟢 NOVOS ESTADOS PARA REFLETIR A DECISÃO DO TUTOR - const [presencasPendentes, setPresencasPendentes] = useState>({}); - const [presencasAprovadas, setPresencasAprovadas] = useState>({}); - const [presencasRejeitadas, setPresencasRejeitadas] = useState>({}); - - const [faltas, setFaltas] = useState>({}); - const [sumarios, setSumarios] = useState>({}); - const [urlsJustificacao, setUrlsJustificacao] = useState>({}); + // 🟢 O NOVO MOTOR CENTRAL (Substitui as 5 variáveis antigas) + const [registosDiarios, setRegistosDiarios] = useState>({}); const [statsFaltas, setStatsFaltas] = useState({ justificadas: 0, injustificadas: 0, totalPresencas: 0 }); const [pdf, setPdf] = useState(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 = {}; + 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 ✅ Horas validadas pela empresa; + 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 ⏳ A aguardar validação do tutor; - } - if (presencasRejeitadas[selectedDate]) { - return ❌ O tutor rejeitou este registo. Corrige o sumário.; - } - return null; + + return ( + + + + {config.texto} + + + ); }; + const regSelecionado = registosDiarios[selectedDate]; + return ( @@ -353,14 +458,14 @@ const AlunoHome = memo(() => { - - + + Confirmar Local Precisamos de validar a tua localização para confirmar que estás no estágio. - + Confirmar e Marcar setShowLocationModal(false)}> @@ -371,7 +476,7 @@ const AlunoHome = memo(() => { {alertConfig && ( - + {alertConfig.msg} )} @@ -380,9 +485,7 @@ const AlunoHome = memo(() => { ref={scrollViewRef} contentContainerStyle={styles.container} showsVerticalScrollIndicator={false} - refreshControl={ - - } + refreshControl={} > Estágios+ @@ -396,80 +499,40 @@ const AlunoHome = memo(() => { - {/* 🚀 BOTÕES DE SEPARADOR (TABS) MODERNOS 🚀 */} - setActiveTab('horas')} - activeOpacity={0.7} + style={[styles.quickActionBtn, activeTab === 'horas' && styles.quickActionBtnActive, activeTab === 'horas' && { backgroundColor: themeStyles.card }]} + onPress={() => setActiveTab('horas')} activeOpacity={0.7} > - - - Horas - + + Horas setActiveTab('horario')} - activeOpacity={0.7} + style={[styles.quickActionBtn, activeTab === 'horario' && styles.quickActionBtnActive, activeTab === 'horario' && { backgroundColor: themeStyles.card }]} + onPress={() => setActiveTab('horario')} activeOpacity={0.7} > - - - Horário - + + Horário setActiveTab('info')} - activeOpacity={0.7} + style={[styles.quickActionBtn, activeTab === 'info' && styles.quickActionBtnActive, activeTab === 'info' && { backgroundColor: themeStyles.card }]} + onPress={() => setActiveTab('info')} activeOpacity={0.7} > - - - Info - + + Info - - {/* CARTÃO PRINCIPAL QUE MUDA CONFORME O SEPARADOR ATIVO */} {!isLoadingDB && ( estagioDetalhes ? ( - {/* O Cabeçalho (Empresa e Status) fica sempre visível */} - - {estagioDetalhes.empresas?.nome || "Empresa não definida"} - + {estagioDetalhes.empresas?.nome || "Empresa não definida"} {badgeObj.label} @@ -478,17 +541,17 @@ const AlunoHome = memo(() => { - {/* CONTEÚDO 1: HORAS */} {activeTab === 'horas' && ( - - + REALIZADAS + {/* 🟢 AGORA USA A VARIÁVEL CALCULADA */} {horasConcluidas}h EM FALTA + {/* 🟢 AGORA USA A VARIÁVEL CALCULADA */} {horasEmFalta}h @@ -496,8 +559,7 @@ const AlunoHome = memo(() => { TOTAIS {horasTotais}h - - + @@ -519,7 +581,6 @@ const AlunoHome = memo(() => { )} - {/* CONTEÚDO 2: HORÁRIO DIÁRIO */} {activeTab === 'horario' && ( @@ -528,11 +589,9 @@ const AlunoHome = memo(() => { {estagioDetalhes.horas_diarias ? estagioDetalhes.horas_diarias + '/dia' : 'Não definido'} - {/* Mostra a Manhã e a Tarde se existirem na base de dados */} {horariosEstagio.length > 0 && ( - {horariosEstagio.map((h, index) => ( @@ -549,10 +608,8 @@ const AlunoHome = memo(() => { )} - {/* CONTEÚDO 3: INFO GERAL */} {activeTab === 'info' && ( - {/* Tutor e Contacto */} @@ -569,7 +626,6 @@ const AlunoHome = memo(() => { - {/* Datas */} @@ -598,7 +654,7 @@ const AlunoHome = memo(() => { @@ -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)} /> @@ -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' && ( Sumário - setEditandoSumario(true)}> + setEditandoSumario(true)}> 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 && Guardar Sumário} + {editandoSumario && Submeter para Validação} )} - {faltas[selectedDate] && ( + {/* SE O ALUNO FALTOU, MOSTRA O UPLOAD DE JUSTIFICAÇÃO */} + {regSelecionado?.estado === 'faltou' && ( - Justificar - {urlsJustificacao[selectedDate] ? ( + Justificar Falta + {regSelecionado.justificacao_url ? ( - Justificativo Enviado + Justificativo Enviado à Entidade ) : ( <> - - - {pdf ? pdf.name : "Selecionar PDF"} + + + {pdf ? pdf.name : "Selecionar Documento PDF"} {pdf && ( - - {isUploading ? : Submeter} + + {isUploading ? : Submeter Documento} )} @@ -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 }, diff --git a/app/Empresa/EmpresaHome.tsx b/app/Empresa/EmpresaHome.tsx index 7284aa5..0c92942 100644 --- a/app/Empresa/EmpresaHome.tsx +++ b/app/Empresa/EmpresaHome.tsx @@ -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([]); + // ESTADOS DE NAVEGAÇÃO E DADOS + const [currentScreen, setCurrentScreen] = useState('DASHBOARD'); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const [empresaNome, setEmpresaNome] = useState(''); + + const [empresaData, setEmpresaData] = useState(null); + const [listaAlunos, setListaAlunos] = useState([]); + const [presencasGerais, setPresencasGerais] = useState([]); + + // ESTADOS PARA MODAIS E SELEÇÕES + const [alunoSelecionado, setAlunoSelecionado] = useState(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 = {}; - 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 = () => ( + + {/* CARD 1: ALUNOS */} + setCurrentScreen('ALUNOS')}> + + + + Alunos + Verificar estagiários e detalhes. + + + {/* CARD 2: PEDIDOS */} + setCurrentScreen('PEDIDOS_LISTA')}> + + + {presencasGerais.filter(p => p.estado_tutor === 'pendente').length > 0 && ( + + )} + + Pedidos + Validar sumários e faltas. + + + {/* CARD 3: AVALIAÇÕES */} + setCurrentScreen('AVALIACOES')}> + + + + Avaliações + Em breve. + + + {/* CARD 4: DEFINIÇÕES */} + setCurrentScreen('DEFINICOES')}> + + + + Definições + Gerir conta da empresa. + + + ); + + const renderAlunos = () => ( + + setCurrentScreen('DASHBOARD')}> + + Voltar ao Menu + + + Estagiários ({listaAlunos.length}) + + {listaAlunos.length === 0 ? ( + Nenhum aluno associado a esta empresa. + ) : ( + listaAlunos.map(aluno => ( + { setAlunoSelecionado(aluno); setModalDetalhesAluno(true); }} + > + + {aluno.nome.charAt(0)} + + + {aluno.nome} + Toque para ver detalhes + + + + )) + )} + + ); + + const renderPedidosLista = () => ( + + setCurrentScreen('DASHBOARD')}> + + Voltar ao Menu + + + Validar Registos + Selecione um aluno para ver o histórico e validar pedidos pendentes. + + {listaAlunos.map(aluno => { + const pendentesDoAluno = presencasGerais.filter(p => p.aluno_id === aluno.id && p.estado_tutor === 'pendente').length; + return ( + { setAlunoSelecionado(aluno); setCurrentScreen('PEDIDOS_HISTORICO'); }} + > + + {aluno.nome} + + {pendentesDoAluno > 0 ? ( + + {pendentesDoAluno} PENDENTES + + ) : ( + + )} + + ); + })} + + ); + + const renderPedidosHistorico = () => { + if (!alunoSelecionado) return null; + const historicoAluno = presencasGerais.filter(p => p.aluno_id === alunoSelecionado.id); + + return ( + + setCurrentScreen('PEDIDOS_LISTA')}> + + Voltar à Lista + + + Histórico: {alunoSelecionado.nome} + + {historicoAluno.length === 0 ? ( + Nenhum registo efetuado por este aluno. + ) : ( + historicoAluno.map(item => ( + + + {formatarData(item.data)} + + + {item.estado === 'faltou' ? 'FALTA' : 'PRESENÇA'} + + + + + + + {item.estado === 'presente' ? (item.sumario || "Sem sumário registado.") : "Aluno marcou ausência."} + + {item.justificacao_url && ( + Linking.openURL(item.justificacao_url)}> + + Ver Justificativo PDF + + )} + + + {/* SÓ MOSTRA BOTÕES SE ESTIVER PENDENTE */} + {item.estado_tutor === 'pendente' ? ( + + lidarComPresenca(item.id, 'rejeitado')}> + + Recusar (Apagar) + + lidarComPresenca(item.id, 'aprovado')}> + + Aprovar + + + ) : ( + + ✅ Aprovado + + )} + + )) + )} + + ); + }; + + const renderAvaliacoes = () => ( + + setCurrentScreen('DASHBOARD')}> + + Voltar + + + BREVEMENTE + A área de avaliações finais está em construção. + + ); + + const renderDefinicoes = () => ( + + setCurrentScreen('DASHBOARD')}> + + Voltar + + + Definições + + + Nome da Empresa + {empresaData?.nome || 'N/A'} + + Tutor + {empresaData?.tutor_nome || 'N/A'} + + + supabase.auth.signOut().then(() => router.replace('/'))} + > + + Terminar Sessão + + + ); + return ( - {/* TOAST ANIMADO */} {toast.message} - - - Bem-vindo, - - {empresaNome || 'Entidade'} - - - supabase.auth.signOut().then(() => router.replace('/'))} - > - - - - - - Validações Pendentes - - {pendentes.length} - + + Painel Empresa + {empresaData?.nome ? {empresaData.nome} : null} {loading && !refreshing ? ( - - - + ) : ( } > - {pendentes.length === 0 ? ( - - - - - Tudo em ordem! - - Não existem registos pendentes de validação. **Vai dar merda** se os alunos não trabalharem! 😂 - - - ) : ( - pendentes.map((item) => ( - - - - {item.aluno_nome.charAt(0)} - - - {item.aluno_nome} - - - {formatarData(item.data)} - - - - PENDENTE - - - - - SUMÁRIO DO DIA: - - {item.sumario || "O aluno não descreveu as atividades deste dia."} - - - - - lidarComPresenca(item.id, 'rejeitado')} - > - - Rejeitar - - - lidarComPresenca(item.id, 'aprovado')} - > - - Aprovar - - - - )) - )} + {currentScreen === 'DASHBOARD' && renderDashboard()} + {currentScreen === 'ALUNOS' && renderAlunos()} + {currentScreen === 'PEDIDOS_LISTA' && renderPedidosLista()} + {currentScreen === 'PEDIDOS_HISTORICO' && renderPedidosHistorico()} + {currentScreen === 'AVALIACOES' && renderAvaliacoes()} + {currentScreen === 'DEFINICOES' && renderDefinicoes()} )} + + {/* MODAL DETALHES DO ALUNO */} + + + + + {alunoSelecionado?.nome} + + + Nº ESCOLA + {alunoSelecionado?.n_escola || 'N/A'} + + + TURMA/CURSO + {alunoSelecionado?.turma_curso || 'N/A'} + + + setModalDetalhesAluno(false)}> + Fechar + + + + ); } 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 } }); \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 8d3db94..7252c53 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -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'); }