diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index af12910..fb389e6 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -2,16 +2,18 @@ import { Ionicons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useFocusEffect } from '@react-navigation/native'; import * as DocumentPicker from 'expo-document-picker'; +import * as Location from 'expo-location'; // <-- Adicionado import { useRouter } from 'expo-router'; import * as Sharing from 'expo-sharing'; import { memo, useCallback, useMemo, useState } from 'react'; import { - Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View + ActivityIndicator, Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { Calendar, LocaleConfig } from 'react-native-calendars'; import { useTheme } from '../../themecontext'; +import { supabase } from '../lib/supabase'; // <-- Garante que tens este arquivo configurado -// Configuração PT +// Configuração PT (Mantida) LocaleConfig.locales['pt'] = { monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'], @@ -34,23 +36,6 @@ const getFeriadosMap = (ano: number) => { [`${ano}-12-08`]: "Imaculada Conceição", [`${ano}-12-25`]: "Natal" }; - - const a = ano % 19, b = Math.floor(ano / 100), c = ano % 100; - const d = Math.floor(b / 4), e = b % 4, f_calc = Math.floor((b + 8) / 25); - const g = Math.floor((b - f_calc + 1) / 3), h = (19 * a + b - d - g + 15) % 30; - const i = Math.floor(c / 4), k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7; - const m = Math.floor((a + 11 * h + 22 * l) / 451); - const mes = Math.floor((h + l - 7 * m + 114) / 31); - const dia = ((h + l - 7 * m + 114) % 31) + 1; - - const pascoa = new Date(ano, mes - 1, dia); - const formatar = (dt: Date) => dt.toISOString().split('T')[0]; - - f[formatar(pascoa)] = "Páscoa"; - f[formatar(new Date(pascoa.getTime() - 47 * 24 * 3600 * 1000))] = "Carnaval"; - f[formatar(new Date(pascoa.getTime() - 2 * 24 * 3600 * 1000))] = "Sexta-feira Santa"; - f[formatar(new Date(pascoa.getTime() + 60 * 24 * 3600 * 1000))] = "Corpo de Deus"; - return f; }; @@ -67,6 +52,7 @@ const AlunoHome = memo(() => { const [faltasJustificadas, setFaltasJustificadas] = useState>({}); const [pdf, setPdf] = useState(null); const [editandoSumario, setEditandoSumario] = useState(false); + const [isLocating, setIsLocating] = useState(false); // Novo estado para loading useFocusEffect( useCallback(() => { @@ -106,15 +92,12 @@ const AlunoHome = memo(() => { const diaSemana = data.getDay(); const ehFimDeSemana = diaSemana === 0 || diaSemana === 6; const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim; - - // CORREÇÃO: Verifica se o dia selecionado é exatamente HOJE const ehHoje = selectedDate === hojeStr; const ehFuturo = selectedDate > hojeStr; const nomeFeriado = feriadosMap[selectedDate]; return { valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado, - // Só permite presença se for o dia atual podeMarcarPresenca: ehHoje && !foraDoIntervalo && !nomeFeriado, ehFuturo, nomeFeriado @@ -135,21 +118,78 @@ const AlunoHome = memo(() => { return marcacoes; }, [presencas, faltas, sumarios, faltasJustificadas, selectedDate, listaFeriados]); + // --- LOGICA DE PRESENÇA COM LOCALIZAÇÃO --- const handlePresenca = async () => { if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "A presença só pode ser marcada no próprio dia."); - const novas = { ...presencas, [selectedDate]: true }; - setPresencas(novas); - await AsyncStorage.setItem('@presencas', JSON.stringify(novas)); + + setIsLocating(true); + try { + // 1. Pedir Permissão + let { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('Erro', 'Precisas de aceitar a localização para marcar presença.'); + setIsLocating(false); + return; + } + + // 2. Obter Localização exata + let location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High }); + const { latitude, longitude } = location.coords; + + // 3. Salvar no Supabase (Para o Professor ver) + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error("Usuário não autenticado."); + + const { error } = await supabase.from('presencas').upsert({ + aluno_id: user.id, + data: selectedDate, + estado: 'presente', + lat: latitude, + lng: longitude + }); + + if (error) throw error; + + // 4. Salvar Localmente (Como estava no teu código) + const novas = { ...presencas, [selectedDate]: true }; + setPresencas(novas); + await AsyncStorage.setItem('@presencas', JSON.stringify(novas)); + + Alert.alert("Sucesso", "Presença marcada com localização!"); + } catch (e) { + console.error(e); + Alert.alert("Erro", "Não foi possível salvar a presença. Se estiveres sem net, vai dar merda."); + } finally { + setIsLocating(false); + } }; const handleFalta = async () => { if (!infoData.valida) return Alert.alert("Bloqueado", "Data inválida."); + + // Para que o professor veja a falta, também temos de mandar para o Supabase + const { data: { user } } = await supabase.auth.getUser(); + if (user) { + await supabase.from('presencas').upsert({ + aluno_id: user.id, + data: selectedDate, + estado: 'faltou' + }); + } + const novas = { ...faltas, [selectedDate]: true }; setFaltas(novas); await AsyncStorage.setItem('@faltas', JSON.stringify(novas)); }; const guardarSumario = async () => { + // Enviar sumário para o Supabase também + const { data: { user } } = await supabase.auth.getUser(); + if (user) { + await supabase.from('presencas').update({ sumario: sumarios[selectedDate] }) + .match({ aluno_id: user.id, data: selectedDate }); + } + await AsyncStorage.setItem('@sumarios', JSON.stringify(sumarios)); setEditandoSumario(false); Alert.alert("Sucesso", "Sumário guardado!"); @@ -190,9 +230,9 @@ const AlunoHome = memo(() => { - Marcar Presença + {isLocating ? : Marcar Presença} (); + const { alunoId, nome } = useLocalSearchParams(); const { isDarkMode } = useTheme(); + const [presencas, setPresencas] = useState([]); + const [loading, setLoading] = useState(true); - const cores = useMemo( - () => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - verde: '#10B981', - verdeSuave: 'rgba(16, 185, 129, 0.1)', - vermelho: '#EF4444', - vermelhoSuave: 'rgba(239, 68, 68, 0.1)', - azul: '#3B82F6', - borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - }), - [isDarkMode] - ); + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', + card: isDarkMode ? '#1A1A1A' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', + azul: '#3B82F6', + azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + verde: '#10B981', + vermelho: '#EF4444', + }), [isDarkMode]); - const abrirMapa = (lat: number, lng: number) => { - const url = Platform.OS === 'ios' - ? `maps://app?saddr=&daddr=${lat},${lng}` - : `google.navigation:q=${lat},${lng}`; - Linking.openURL(url); - }; + useEffect(() => { + if (alunoId) fetchHistorico(); + }, [alunoId]); + + async function fetchHistorico() { + try { + setLoading(true); + const { data, error } = await supabase + .from('presencas') + .select('*') + .eq('aluno_id', alunoId) + .order('data', { ascending: false }); + + if (error) throw error; + setPresencas(data || []); + } catch (error: any) { + console.error(error.message); + Alert.alert("Erro", "Falha ao carregar o histórico real."); + } finally { + setLoading(false); + } + } return ( - + {/* StatusBar configurada para não sobrepor o conteúdo */} + - {/* HEADER SUPERIOR */} - - router.back()} style={styles.backBtn}> - - - - {nome} - Histórico de Registos + {/* Header com design idêntico ao anterior */} + + + router.back()} style={styles.backBtn}> + + + + Histórico + {nome} + + + + - - - - {/* RESUMO RÁPIDO */} - - - PRESENÇAS - 12 - - - FALTAS - 2 - + {loading ? ( + + - - Atividade Recente - - {presencasData.map((p, i) => { - const isPresente = p.estado === 'presente'; - - return ( - - {/* LINHA LATERAL (TIMELINE) */} - - - {i !== presencasData.length - 1 && } - - - - - - - {new Date(p.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long' })} - - - Registado às {p.hora || '--:--'} - - - - - {p.estado.toUpperCase()} - - - - - - - - {isPresente ? ( - abrirMapa(p.localizacao!.lat, p.localizacao!.lng)} - > - - Ver Localização - - ) : ( - router.push('/Professor/Alunos/Faltas')} - > - - Justificar Falta - - )} - - + ) : ( + + {presencas.length === 0 ? ( + + + + Sem registos reais para este aluno. + - ); - })} - + ) : ( + presencas.map((item) => { + const isPresente = item.estado === 'presente'; + + return ( + + + {/* Ícone de Calendário no lugar do Avatar para manter o peso visual */} + + + + + + + {new Date(item.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })} + + + + + {item.estado.toUpperCase()} + + + + + {item.lat && ( + + )} + + + {/* Área do Sumário com o estilo de "nota" do ecrã anterior */} + + Sumário do Dia + + {item.sumario || "O aluno ainda não preencheu o sumário deste dia."} + + + + ); + }) + )} + + )} ); } const styles = StyleSheet.create({ - safe: { - flex: 1, + safe: { + flex: 1, + // Garante que o conteúdo não fica debaixo da barra de notificações (Android) + paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 }, - headerFixed: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 15, - paddingVertical: 15, - borderBottomWidth: 1, - marginTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0, + headerFixed: { + paddingHorizontal: 20, + paddingBottom: 10 }, - backBtn: { - width: 40, - height: 40, - justifyContent: 'center', - alignItems: 'center', + topBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + height: 70 }, - headerInfo: { - alignItems: 'center', - }, - title: { fontSize: 18, fontWeight: '800' }, - subtitle: { fontSize: 12, fontWeight: '500' }, - scrollContainer: { padding: 20 }, - - summaryRow: { - flexDirection: 'row', - gap: 12, - marginBottom: 30, - }, - statBox: { - flex: 1, - padding: 15, - borderRadius: 20, - alignItems: 'center', + backBtn: { width: 40, height: 40, justifyContent: 'center' }, + refreshBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'flex-end' }, + title: { fontSize: 22, fontWeight: '800' }, + subtitle: { fontSize: 13, marginTop: -2, fontWeight: '600' }, + scrollContent: { padding: 20 }, + card: { + borderRadius: 20, + padding: 16, + marginBottom: 15, + borderWidth: 1, elevation: 2, shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, - shadowRadius: 10, + shadowRadius: 5 }, - statLabel: { fontSize: 9, fontWeight: '800', letterSpacing: 1 }, - statValue: { fontSize: 22, fontWeight: '900', marginTop: 4 }, - - sectionTitle: { fontSize: 16, fontWeight: '800', marginBottom: 20 }, - - timelineItem: { - flexDirection: 'row', + cardHeader: { flexDirection: 'row', alignItems: 'center' }, + iconBox: { + width: 44, + height: 44, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center' }, - timelineLeft: { - width: 30, - alignItems: 'center', + info: { flex: 1, marginLeft: 15 }, + dateText: { fontSize: 16, fontWeight: '700' }, + statusBadge: { flexDirection: 'row', alignItems: 'center', marginTop: 4 }, + dot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + subText: { fontSize: 11, fontWeight: '800', letterSpacing: 0.5 }, + sumarioBox: { + marginTop: 15, + padding: 12, + borderRadius: 12, }, - dot: { - width: 12, - height: 12, - borderRadius: 6, - zIndex: 2, + sumarioLabel: { + fontSize: 10, + fontWeight: '800', + textTransform: 'uppercase', + marginBottom: 5, + letterSpacing: 0.5 }, - line: { - width: 2, - flex: 1, - marginTop: -5, - }, - card: { - flex: 1, - borderRadius: 20, - padding: 16, - marginBottom: 20, - borderWidth: 1, - marginLeft: 10, - elevation: 3, - shadowColor: '#000', - shadowOpacity: 0.04, - shadowRadius: 8, - }, - cardHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - dateText: { fontSize: 15, fontWeight: '700' }, - hourText: { fontSize: 12, marginTop: 2 }, - statusBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - }, - statusText: { fontSize: 9, fontWeight: '900' }, - cardDivider: { - height: 1, - marginVertical: 12, - }, - cardAction: { - flexDirection: 'row', - alignItems: 'center', - }, - actionButton: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - }, - actionText: { fontSize: 13, fontWeight: '700' }, + sumarioText: { fontSize: 13, lineHeight: 18, opacity: 0.9 }, + centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + empty: { alignItems: 'center', marginTop: 60 }, + emptyText: { marginTop: 15, fontSize: 14, fontWeight: '600', textAlign: 'center' }, }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 97e28c5..8ad5f68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.10", + "expo-location": "~19.0.8", "expo-router": "~6.0.17", "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.12", @@ -6576,6 +6577,15 @@ "react-native": "*" } }, + "node_modules/expo-location": { + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-19.0.8.tgz", + "integrity": "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.24", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz", diff --git a/package.json b/package.json index c075539..13a8849 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.10", + "expo-location": "~19.0.8", "expo-router": "~6.0.17", "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.12",