From 83095f5a2d33022d9be2b5154e76c913f82a642e Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Mon, 13 Apr 2026 15:01:24 +0100 Subject: [PATCH] finalizar design --- app/Professor/Alunos/CalendarioPresencas.tsx | 331 ++++++++------- app/Professor/Alunos/Faltas.tsx | 419 +++++++++++-------- app/Professor/Alunos/Presencas.tsx | 359 +++++++--------- app/Professor/Alunos/Sumarios.tsx | 401 +++++++++--------- 4 files changed, 775 insertions(+), 735 deletions(-) diff --git a/app/Professor/Alunos/CalendarioPresencas.tsx b/app/Professor/Alunos/CalendarioPresencas.tsx index 274b8f2..3005c00 100644 --- a/app/Professor/Alunos/CalendarioPresencas.tsx +++ b/app/Professor/Alunos/CalendarioPresencas.tsx @@ -1,23 +1,25 @@ // app/(Professor)/HistoricoPresencas.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, + FlatList, Linking, Platform, - SafeAreaView, - ScrollView, + RefreshControl, StatusBar, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// --- INTERFACES --- interface Presenca { id: string; aluno_id: string; @@ -30,75 +32,90 @@ interface Presenca { lng?: number; } -export default function HistoricoPresencas() { +const HistoricoPresencas = memo(() => { const router = useRouter(); const params = useLocalSearchParams(); const { isDarkMode } = useTheme(); + const insets = useSafeAreaInsets(); + const [presencas, setPresencas] = useState([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const idStr = Array.isArray(params.alunoId) ? params.alunoId[0] : params.alunoId; const nomeStr = Array.isArray(params.nome) ? params.nome[0] : params.nome; + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9', - card: isDarkMode ? '#161618' : '#FFFFFF', - texto: isDarkMode ? '#FFFFFF' : '#1A1C1E', - secundario: isDarkMode ? '#8E8E93' : '#6C757D', - azul: '#2390a6', - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', - vermelho: '#FF453A', - verde: '#32D74B', - borda: isDarkMode ? '#2C2C2E' : '#E9ECEF', - linha: isDarkMode ? '#2C2C2E' : '#D1D9E6', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + vermelho: '#EF4444', + verde: '#10B981', }), [isDarkMode]); - useEffect(() => { - if (idStr) fetchHistorico(); - }, [idStr]); - - async function fetchHistorico() { + const fetchHistorico = async () => { try { setLoading(true); - const hoje = new Date().toISOString().split('T')[0]; - // 1. Procurar o estágio ativo (mesma lógica das Faltas) - const { data: listaEstagios, error: errEstagio } = await supabase + if (!idStr) { + console.error("ID do aluno não identificado."); + return; + } + + // 1. Procurar o estágio deste aluno + const { data: estagioData, error: errEstagio } = await supabase .from('estagios') .select('data_inicio, data_fim') - .lte('data_inicio', hoje) - .gte('data_fim', hoje) - .limit(1); + .eq('aluno_id', idStr) + .order('data_inicio', { ascending: false }); if (errEstagio) throw errEstagio; - // Fallback caso não haja estágio hoje (Janeiro a Dezembro de 2026) - let inicio = '2026-01-01'; - let fim = '2026-12-31'; - - if (listaEstagios && listaEstagios.length > 0) { - inicio = listaEstagios[0].data_inicio; - fim = listaEstagios[0].data_fim; + if (!estagioData || estagioData.length === 0) { + setPresencas([]); + return; } - // 2. Buscar histórico filtrado por esse período - const { data, error } = await supabase + const estagio = estagioData[0]; + + // 2. Buscar presenças no intervalo do estágio + const { data: presencasData, error: errPresencas } = await supabase .from('presencas') .select('*') .eq('aluno_id', idStr) - .gte('data', inicio) - .lte('data', fim) + .gte('data', estagio.data_inicio) + .lte('data', estagio.data_fim) .order('data', { ascending: false }); - if (error) throw error; - setPresencas(data || []); + if (errPresencas) throw errPresencas; + + setPresencas(presencasData || []); + } catch (error: any) { - console.error(error); - Alert.alert("Erro", "Não foi possível carregar as presenças do estágio atual."); + console.error("Erro Geral:", error); + Alert.alert("Erro", "Falha ao carregar dados do estágio."); } finally { setLoading(false); + setRefreshing(false); } - } + }; + + useEffect(() => { + if (idStr) fetchHistorico(); + }, [idStr]); + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchHistorico(); + }, [idStr]); const handleNavigation = (item: Presenca) => { if (item.estado === 'faltou') { @@ -106,7 +123,7 @@ export default function HistoricoPresencas() { pathname: '/Professor/Alunos/Faltas', params: { alunoId: idStr, nome: nomeStr } }); - } else if (item.estado === 'presente') { + } else { router.push({ pathname: '/Professor/Alunos/Sumarios', params: { alunoId: idStr, nome: nomeStr } @@ -117,144 +134,136 @@ export default function HistoricoPresencas() { const abrirMapa = (lat: number, lng: number) => { const scheme = Platform.select({ ios: 'maps:0,0?q=', android: 'geo:0,0?q=' }); const latLng = `${lat},${lng}`; - const label = `Localização de ${nomeStr}`; const url = Platform.select({ - ios: `${scheme}${label}@${latLng}`, - android: `${scheme}${latLng}(${label})` + ios: `${scheme}${nomeStr}@${latLng}`, + android: `${scheme}${latLng}(${nomeStr})` }); - - if (url) { - Linking.openURL(url).catch(() => Alert.alert("Erro", "Não foi possível abrir o mapa.")); - } + if (url) Linking.openURL(url); }; return ( - - - - - + + + + + {/* HEADER */} + router.back()} - style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.btnAction, { borderColor: cores.borda }]} + onPress={() => router.back()} > - + - + Histórico - {nomeStr} + {nomeStr} - + - - {loading ? ( - - - - ) : ( - - {presencas.length === 0 ? ( - - - + {loading && !refreshing ? ( + + ) : ( + item.id} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + refreshControl={} + ListHeaderComponent={() => ( + + + Registos Recentes + - Sem registos neste estágio - Não foram encontradas presenças ou faltas para o período selecionado. - - ) : ( - - - - {presencas.map((item) => { - const isPresente = item.estado === 'presente'; - const dataObj = new Date(item.data); - - return ( - - - - handleNavigation(item)} - style={[styles.modernCard, { backgroundColor: cores.card }]} - > - - - - {dataObj.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })} - - - {dataObj.getFullYear()} - - - - - - - - - {isPresente ? 'PRESENÇA MARCADA' : 'FALTA REGISTADA'} - - {item.lat && item.lng && ( - abrirMapa(item.lat!, item.lng!)} style={styles.mapSmallBtn}> - - - )} - - - Clique para ver {isPresente ? 'o sumário' : 'a justificação'} - - - - - - + )} + renderItem={({ item }) => { + const isPresente = item.estado === 'presente'; + const dataObj = new Date(item.data); + + return ( + handleNavigation(item)} + style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]} + > + + + {dataObj.getDate()} + + + {dataObj.toLocaleDateString('pt-PT', { month: 'short' }).replace('.', '')} + - ); - })} - - )} - - )} - + + + + + + {isPresente ? 'Presença Registada' : 'Falta Marcada'} + + + + {isPresente ? 'Ver sumário do dia' : 'Ver justificação'} + + + + + {item.lat && item.lng && ( + abrirMapa(item.lat!, item.lng!)} + style={[styles.iconBtn, { backgroundColor: cores.azulSuave }]} + > + + + )} + + + + ); + }} + ListEmptyComponent={() => ( + + + Sem registos este ano. + + )} + /> + )} + + ); -} +}); const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 }, - headerArea: { paddingHorizontal: 24, paddingBottom: 15, paddingTop: 10 }, - topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 60 }, - btnAction: { width: 45, height: 45, borderRadius: 16, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - titleWrapper: { alignItems: 'center', flex: 1 }, - headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 }, - scrollContent: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 50 }, - timelineWrapper: { paddingLeft: 10 }, - timelineLine: { position: 'absolute', left: 10, top: 0, bottom: 0, width: 2, borderRadius: 1 }, - timelineItem: { marginBottom: 20, paddingLeft: 25, justifyContent: 'center' }, - timelineDot: { position: 'absolute', left: 6, top: '50%', marginTop: -5, width: 10, height: 10, borderRadius: 5, borderWidth: 2, zIndex: 1 }, - modernCard: { borderRadius: 20, padding: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 }, - cardMain: { flexDirection: 'row', alignItems: 'center' }, - dateInfo: { alignItems: 'center', minWidth: 50 }, - dayText: { fontSize: 15, fontWeight: '800', textTransform: 'uppercase' }, - yearText: { fontSize: 11, fontWeight: '600' }, - dividerVertical: { width: 1, height: 30, backgroundColor: 'rgba(128,128,128,0.2)', marginHorizontal: 15 }, - contentInfo: { flex: 1 }, - statusRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 2 }, - statusText: { fontSize: 11, fontWeight: '900', letterSpacing: 0.5 }, - mapSmallBtn: { marginLeft: 8 }, - infoHint: { fontSize: 12, fontWeight: '500' }, - centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - emptyContainer: { alignItems: 'center', marginTop: 100 }, - emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - emptyText: { fontSize: 18, fontWeight: '800', marginBottom: 8 }, - emptySub: { fontSize: 14, textAlign: 'center', paddingHorizontal: 40, lineHeight: 20 }, -}); \ No newline at end of file + safe: { flex: 1 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, maxWidth: 200 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + listPadding: { paddingHorizontal: 24, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, marginTop: 10 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 }, + dateBox: { width: 54, height: 54, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + dateDay: { fontSize: 18, fontWeight: '900' }, + dateMonth: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' }, + infoArea: { flex: 1, marginLeft: 15 }, + statusRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 2 }, + indicator: { width: 8, height: 8, borderRadius: 4 }, + statusText: { fontSize: 15, fontWeight: '800', letterSpacing: -0.3 }, + subInfo: { fontSize: 12, fontWeight: '600' }, + actionIcons: { flexDirection: 'row', alignItems: 'center', gap: 10 }, + iconBtn: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + emptyContainer: { marginTop: 100, alignItems: 'center' }, +}); + +export default HistoricoPresencas; \ No newline at end of file diff --git a/app/Professor/Alunos/Faltas.tsx b/app/Professor/Alunos/Faltas.tsx index 0471657..3be8e41 100644 --- a/app/Professor/Alunos/Faltas.tsx +++ b/app/Professor/Alunos/Faltas.tsx @@ -1,26 +1,27 @@ -// app/(Admin)/FaltasAlunos.tsx +// app/Professor/Alunos/Faltas.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, FlatList, Linking, Modal, - Platform, - SafeAreaView, + RefreshControl, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, - View + View, } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// --- INTERFACES --- export interface Aluno { id: string; nome: string; @@ -35,36 +36,40 @@ interface Falta { estado: string; } -const FaltasAlunos = memo(() => { +const ListaFaltasProfessor = memo(() => { const { isDarkMode } = useTheme(); const router = useRouter(); + const insets = useSafeAreaInsets(); + // Estados da Lista const [search, setSearch] = useState(''); const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); - const [modalFaltasVisible, setModalFaltasVisible] = useState(false); + // Estados do Modal de Faltas + const [modalVisible, setModalVisible] = useState(false); const [alunoSelecionado, setAlunoSelecionado] = useState(null); const [faltas, setFaltas] = useState([]); const [loadingFaltas, setLoadingFaltas] = useState(false); - const azulPetroleo = '#2390a6'; + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - 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', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + vermelho: '#EF4444', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.12)' : '#FEF2F2', + verde: '#10B981', }), [isDarkMode]); - useEffect(() => { fetchAlunos(); }, []); - const fetchAlunos = async () => { try { setLoading(true); @@ -78,7 +83,7 @@ const FaltasAlunos = memo(() => { const agrupadas: Record = {}; data?.forEach(item => { - const nomeTurma = `${item.ano}º ${item.turma_curso}`; + const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase(); if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = []; agrupadas[nomeTurma].push({ id: item.id, @@ -88,228 +93,276 @@ const FaltasAlunos = memo(() => { }); }); - setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] }))); - } catch (err) { - console.error(err); - } finally { - setLoading(false); + setTurmas(Object.keys(agrupadas) + .sort((a, b) => b.localeCompare(a)) + .map(nome => ({ nome, alunos: agrupadas[nome] })) + ); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + setRefreshing(false); } }; const abrirFaltas = async (aluno: Aluno) => { setAlunoSelecionado(aluno); - setModalFaltasVisible(true); + setModalVisible(true); setLoadingFaltas(true); setFaltas([]); try { - const hoje = new Date().toISOString().split('T')[0]; - - // CORREÇÃO DO ERRO PGRST116: Usamos .limit(1) em vez de .single() - const { data: listaEstagios, error: errEstagio } = await supabase + // 1. Procurar o estágio ativo/recente do aluno para obter as datas + const { data: estagioData, error: errEstagio } = await supabase .from('estagios') .select('data_inicio, data_fim') - .lte('data_inicio', hoje) - .gte('data_fim', hoje) - .limit(1); // Pega apenas o primeiro se houver múltiplos + .eq('aluno_id', aluno.id) + .order('data_inicio', { ascending: false }) + .limit(1) + .maybeSingle(); if (errEstagio) throw errEstagio; - // Se não houver estágio hoje, tentamos pegar o mais recente - let inicio = '2026-01-01'; - let fim = '2026-12-31'; - - if (listaEstagios && listaEstagios.length > 0) { - inicio = listaEstagios[0].data_inicio; - fim = listaEstagios[0].data_fim; + // Se não houver estágio, não há intervalo para filtrar, logo não mostramos faltas "soltas" + if (!estagioData) { + setFaltas([]); + return; } + // 2. Buscar presenças com estado 'faltou' APENAS dentro das datas do estágio const { data, error } = await supabase .from('presencas') .select('id, data, justificacao_url, estado') .eq('aluno_id', aluno.id) .eq('estado', 'faltou') - .gte('data', inicio) - .lte('data', fim) + .gte('data', estagioData.data_inicio) + .lte('data', estagioData.data_fim) .order('data', { ascending: false }); if (error) throw error; setFaltas(data || []); - - } catch (err) { - console.error("Erro na base:", err); - Alert.alert("Erro", "Não foi possível carregar as faltas deste estágio."); - } finally { - setLoadingFaltas(false); + } catch (err) { + console.error(err); + Alert.alert("Erro", "Falha ao carregar o histórico de faltas do estágio."); + } finally { + setLoadingFaltas(false); } }; + useEffect(() => { fetchAlunos(); }, []); + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchAlunos(); + }, []); + const verDocumento = async (url: string) => { try { await Linking.openURL(url); } catch (error) { - Alert.alert("Erro", "Não foi possível abrir o comprovativo."); + Alert.alert("Erro", "Não foi possível abrir o ficheiro."); } }; - const filteredTurmas = turmas - .map(turma => ({ - ...turma, - alunos: turma.alunos.filter(a => - a.nome.toLowerCase().includes(search.toLowerCase()) || - a.n_escola.includes(search) - ), - })) - .filter(t => t.alunos.length > 0); + const filteredTurmas = useMemo(() => { + return turmas + .map(turma => ({ + ...turma, + alunos: turma.alunos.filter(a => + a.nome.toLowerCase().includes(search.toLowerCase()) || + a.n_escola.includes(search) + ), + })) + .filter(t => t.alunos.length > 0); + }, [turmas, search]); return ( - - + + - - - router.back()} style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}> - + + + {/* HEADER */} + + router.back()} + > + + + + + Faltas + Histórico de Assiduidade + + + + - Gestão de Faltas - - - - + {/* SEARCH */} + + + + + - - {loading ? ( - - - - ) : ( - item.nome} - contentContainerStyle={styles.scrollContent} - showsVerticalScrollIndicator={false} - renderItem={({ item }) => ( - - {item.nome} - {item.alunos.map(aluno => ( - abrirFaltas(aluno)}> - - {aluno.nome.charAt(0).toUpperCase()} - - - {aluno.nome} - Nº Aluno: {aluno.n_escola} - - - - - - ))} - - )} - /> - )} + {loading && !refreshing ? ( + + ) : ( + item.nome} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + refreshControl={} + renderItem={({ item }) => ( + + + + {item.nome} + + - setModalFaltasVisible(false)}> - - - - - Faltas de Estágio - {alunoSelecionado?.nome} - - setModalFaltasVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> - - - - - {loadingFaltas ? ( - - ) : ( - - {faltas.length === 0 ? ( - - - + {item.alunos.map((aluno) => ( + abrirFaltas(aluno)} + > + + + {aluno.nome.charAt(0).toUpperCase()} + - Sem faltas no período atual - - ) : ( - faltas.map(f => ( - - - - + + + {aluno.nome} + + + Nº {aluno.n_escola} + + + + + + VER FALTAS + + + ))} + + )} + ListEmptyComponent={() => ( + + + Nenhum aluno encontrado. + + )} + /> + )} + + {/* MODAL DE DETALHES DE FALTAS */} + setModalVisible(false)}> + + + + + Registo de Faltas + {alunoSelecionado?.nome} + + setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> + + + + + {loadingFaltas ? ( + + ) : ( + + {faltas.length === 0 ? ( + + + Este aluno não tem faltas no estágio atual! + + ) : ( + faltas.map((f) => ( + + + - {new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })} + {new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })} - - {f.justificacao_url ? 'Justificada' : 'Injustificada'} + + {f.justificacao_url ? 'JUSTIFICADA' : 'INJUSTIFICADA'} {f.justificacao_url && ( - verDocumento(f.justificacao_url!)}> + verDocumento(f.justificacao_url!)}> )} - - )) - )} - - )} + )) + )} + + )} + - - - + + + + ); }); -export default FaltasAlunos; - const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, - headerFixed: { paddingHorizontal: 20, paddingBottom: 10 }, - topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', height: 60, marginTop: 10 }, - backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - topTitle: { fontSize: 18, fontWeight: '800' }, - searchContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 16, borderWidth: 1.5, marginTop: 10 }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 30, paddingTop: 10 }, - section: { marginBottom: 25 }, - sectionLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 5, letterSpacing: 1.2 }, - card: { flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 24, marginBottom: 12, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, - avatar: { width: 50, height: 50, borderRadius: 18, alignItems: 'center', justifyContent: 'center' }, - avatarText: { color: '#fff', fontSize: 22, fontWeight: '800' }, - info: { flex: 1, marginLeft: 15 }, - nome: { fontSize: 16, fontWeight: '700' }, - subText: { fontSize: 12, fontWeight: '600', marginTop: 3 }, - alertIcon: { width: 34, height: 34, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, - centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' }, - modalContent: { height: '82%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25 }, - modalTitle: { fontSize: 20, fontWeight: '800' }, - modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 }, + safe: { flex: 1 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + searchSection: { paddingHorizontal: 24, marginBottom: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, + searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + listPadding: { paddingHorizontal: 24, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 18, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 15 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, + idText: { fontSize: 13, fontWeight: '600' }, + statusBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10 }, + statusText: { fontSize: 9, fontWeight: '900' }, + emptyContainer: { marginTop: 80, alignItems: 'center' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, + modalContent: { height: '85%', borderTopLeftRadius: 40, borderTopRightRadius: 40, elevation: 20 }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 24 }, + modalTitle: { fontSize: 20, fontWeight: '900' }, + modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 }, closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - faltaCard: { padding: 16, borderRadius: 22, marginBottom: 12, elevation: 1 }, - faltaRow: { flexDirection: 'row', alignItems: 'center' }, - dateBadge: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - faltaData: { fontSize: 15, fontWeight: '800' }, - faltaStatus: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 }, - viewBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - emptyState: { alignItems: 'center', marginTop: 80 }, - emptyIconContainer: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - emptyText: { fontSize: 16, fontWeight: '700' } -}); \ No newline at end of file + faltaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1 }, + dateIcon: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + faltaData: { fontSize: 16, fontWeight: '800' }, + statusFaltaText: { fontSize: 10, fontWeight: '900', marginTop: 2 }, + btnView: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' } +}); + +export default ListaFaltasProfessor; \ No newline at end of file diff --git a/app/Professor/Alunos/Presencas.tsx b/app/Professor/Alunos/Presencas.tsx index ff038b1..b6f131f 100644 --- a/app/Professor/Alunos/Presencas.tsx +++ b/app/Professor/Alunos/Presencas.tsx @@ -1,12 +1,11 @@ // app/(Professor)/Presencas.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, FlatList, - Platform, - SafeAreaView, + RefreshControl, StatusBar, StyleSheet, Text, @@ -14,9 +13,11 @@ import { TouchableOpacity, View, } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// --- INTERFACES --- export interface Aluno { id: string; nome: string; @@ -27,27 +28,27 @@ export interface Aluno { const Presencas = memo(() => { const { isDarkMode } = useTheme(); const router = useRouter(); + const insets = useSafeAreaInsets(); const [search, setSearch] = useState(''); const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; - // Paleta de Cores Premium const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9', - card: isDarkMode ? '#161618' : '#FFFFFF', - texto: isDarkMode ? '#FFFFFF' : '#1A1C1E', - secundario: isDarkMode ? '#8E8E93' : '#6C757D', - azul: '#2390a6', // O teu azul de estimação - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', - borda: isDarkMode ? '#2C2C2E' : '#E9ECEF', - sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); - useEffect(() => { - fetchAlunos(); - }, []); - const fetchAlunos = async () => { try { setLoading(true); @@ -59,14 +60,9 @@ const Presencas = memo(() => { if (error) throw error; - if (!data) { - setTurmas([]); - return; - } - const agrupadas: Record = {}; - data.forEach(item => { - const nomeTurma = `${item.ano}º ${item.turma_curso}`; + data?.forEach(item => { + const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase(); if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = []; agrupadas[nomeTurma].push({ id: item.id, @@ -76,210 +72,165 @@ const Presencas = memo(() => { }); }); - setTurmas( - Object.keys(agrupadas).map(nome => ({ - nome, - alunos: agrupadas[nome], - })) + setTurmas(Object.keys(agrupadas) + .sort((a, b) => b.localeCompare(a)) + .map(nome => ({ nome, alunos: agrupadas[nome] })) ); } catch (err) { - console.error('Erro ao carregar alunos:', err); + console.error('Erro ao buscar alunos:', err); } finally { setLoading(false); + setRefreshing(false); } }; - const filteredTurmas = turmas - .map(turma => ({ - ...turma, - alunos: turma.alunos.filter(a => - a.nome.toLowerCase().includes(search.toLowerCase()) || - a.n_escola.includes(search) - ), - })) - .filter(t => t.alunos.length > 0); + useEffect(() => { fetchAlunos(); }, []); + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchAlunos(); + }, []); + + const filteredTurmas = useMemo(() => { + return turmas + .map(turma => ({ + ...turma, + alunos: turma.alunos.filter(a => + a.nome.toLowerCase().includes(search.toLowerCase()) || + a.n_escola.includes(search) + ), + })) + .filter(t => t.alunos.length > 0); + }, [turmas, search]); return ( - - + + - {/* HEADER DINÂMICO */} - - + + + {/* HEADER EPVC */} + router.back()} - style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.btnAction, { borderColor: cores.borda }]} + onPress={() => router.back()} > - + - + + Presenças - Lista de Alunos + Controlo Diário - + + + + - - - - {search !== '' && ( - setSearch('')}> - - - )} + {/* SEARCH BAR */} + + + + + - - {loading ? ( - - - A carregar turma... - - ) : ( - item.nome} - contentContainerStyle={styles.listContent} - showsVerticalScrollIndicator={false} - renderItem={({ item }) => ( - - - - {item.nome} - - {item.alunos.length} + {loading && !refreshing ? ( + + ) : ( + item.nome} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + refreshControl={} + renderItem={({ item }) => ( + + {/* SEPARADOR DE TURMA */} + + + {item.nome} + - - {item.alunos.map(aluno => ( - - router.push({ - pathname: '/Professor/Alunos/CalendarioPresencas', - params: { alunoId: aluno.id, nome: aluno.nome }, - }) - } - > - - - {aluno.nome.charAt(0).toUpperCase()} - - - - - {aluno.nome} - - - Registo de faltas + {item.alunos.map((aluno) => ( + + router.push({ + pathname: '/Professor/Alunos/CalendarioPresencas', + params: { alunoId: aluno.id, nome: aluno.nome }, + }) + } + > + + + {aluno.nome.charAt(0).toUpperCase()} + - - - - - - - ))} - - )} - /> - )} - + + + {aluno.nome} + + + Abrir Calendário + + + + + + + + ))} + + )} + ListEmptyComponent={() => ( + + + Sem resultados. + + )} + /> + )} + + ); }); -export default Presencas; - const styles = StyleSheet.create({ - safe: { - flex: 1, - paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0, - }, - headerContainer: { - paddingHorizontal: 24, - paddingBottom: 20, - paddingTop: 10, - }, - topRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 20, - }, - btnBack: { - width: 45, - height: 45, - borderRadius: 15, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 1, - }, - titleWrapper: { alignItems: 'center' }, - headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 }, - searchWrapper: { - flexDirection: 'row', - alignItems: 'center', - borderWidth: 1, - borderRadius: 18, - paddingHorizontal: 16, - height: 54, - }, - inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' }, - listContent: { paddingHorizontal: 24, paddingBottom: 40 }, - turmaSection: { marginBottom: 30 }, - turmaHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 16, - paddingLeft: 4 - }, - turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 }, - turmaTitle: { fontSize: 16, fontWeight: '800', flex: 1 }, - countBadge: { - paddingHorizontal: 10, - paddingVertical: 4, - borderRadius: 10, - }, - countText: { fontSize: 11, fontWeight: '900' }, - alunoCard: { - flexDirection: 'row', - alignItems: 'center', - padding: 12, - borderRadius: 24, - marginBottom: 12, - elevation: 4, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 1, - shadowRadius: 12, - }, - avatarBox: { - width: 52, - height: 52, - borderRadius: 18, - justifyContent: 'center', - alignItems: 'center', - }, - avatarChar: { fontSize: 20, fontWeight: '900' }, - alunoInfo: { flex: 1, marginLeft: 16 }, - alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 }, - alunoStatusRow: { flexDirection: 'row', alignItems: 'center' }, - alunoSub: { fontSize: 13, fontWeight: '500' }, - goCircle: { - width: 32, - height: 32, - borderRadius: 16, - justifyContent: 'center', - alignItems: 'center', - }, - loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - loaderText: { marginTop: 15, fontSize: 14, fontWeight: '600' }, -}); \ No newline at end of file + safe: { flex: 1 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + searchSection: { paddingHorizontal: 24, marginBottom: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, + searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + listPadding: { paddingHorizontal: 24, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 20, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 15 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, + idText: { fontSize: 13, fontWeight: '600' }, + statusBadge: { width: 34, height: 34, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + emptyContainer: { marginTop: 100, alignItems: 'center' }, +}); + +export default Presencas; \ No newline at end of file diff --git a/app/Professor/Alunos/Sumarios.tsx b/app/Professor/Alunos/Sumarios.tsx index 278c2ad..bcd3f5d 100644 --- a/app/Professor/Alunos/Sumarios.tsx +++ b/app/Professor/Alunos/Sumarios.tsx @@ -1,13 +1,12 @@ // app/(Professor)/SumariosAlunos.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, FlatList, Modal, - Platform, - SafeAreaView, + RefreshControl, ScrollView, StatusBar, StyleSheet, @@ -16,9 +15,11 @@ import { TouchableOpacity, View, } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// --- INTERFACES --- export interface Aluno { id: string; nome: string; @@ -36,41 +37,33 @@ const SumariosAlunos = memo(() => { const { isDarkMode } = useTheme(); const router = useRouter(); const params = useLocalSearchParams(); + const insets = useSafeAreaInsets(); const [search, setSearch] = useState(''); const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + // Estados do Modal const [modalVisible, setModalVisible] = useState(false); const [alunoSelecionado, setAlunoSelecionado] = useState(null); const [sumarios, setSumarios] = useState([]); const [loadingSumarios, setLoadingSumarios] = useState(false); + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; + const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9', - card: isDarkMode ? '#161618' : '#FFFFFF', - texto: isDarkMode ? '#FFFFFF' : '#1A1C1E', - secundario: isDarkMode ? '#8E8E93' : '#6C757D', - azul: '#2390a6', - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', - borda: isDarkMode ? '#2C2C2E' : '#E9ECEF', - sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); - useEffect(() => { fetchAlunos(); }, []); - - useEffect(() => { - if (params.alunoId && typeof params.alunoId === 'string') { - const alunoAuto = { - id: params.alunoId, - nome: (params.nome as string) || 'Aluno', - n_escola: '', - turma: '' - }; - abrirSumarios(alunoAuto); - } - }, [params.alunoId]); - const fetchAlunos = async () => { try { setLoading(true); @@ -84,7 +77,7 @@ const SumariosAlunos = memo(() => { const agrupadas: Record = {}; data?.forEach(item => { - const nomeTurma = `${item.ano}º ${item.turma_curso}`; + const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase(); if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = []; agrupadas[nomeTurma].push({ id: item.id, @@ -94,8 +87,16 @@ const SumariosAlunos = memo(() => { }); }); - setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] }))); - } catch (err) { console.error(err); } finally { setLoading(false); } + setTurmas(Object.keys(agrupadas) + .sort((a, b) => b.localeCompare(a)) + .map(nome => ({ nome, alunos: agrupadas[nome] })) + ); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + setRefreshing(false); + } }; const abrirSumarios = async (aluno: Aluno) => { @@ -104,8 +105,6 @@ const SumariosAlunos = memo(() => { setLoadingSumarios(true); try { const hoje = new Date().toISOString().split('T')[0]; - - // 1. Procurar o estágio ativo const { data: listaEstagios } = await supabase .from('estagios') .select('data_inicio, data_fim') @@ -121,7 +120,6 @@ const SumariosAlunos = memo(() => { fim = listaEstagios[0].data_fim; } - // 2. Buscar sumários filtrados pelo período do estágio const { data, error } = await supabase .from('presencas') .select('id, data, sumario') @@ -133,182 +131,211 @@ const SumariosAlunos = memo(() => { if (error) throw error; setSumarios(data || []); - } catch (err) { console.error(err); } finally { setLoadingSumarios(false); } + } catch (err) { + console.error(err); + } finally { + setLoadingSumarios(false); + } }; - const filteredTurmas = turmas - .map(turma => ({ - ...turma, - alunos: turma.alunos.filter(a => - a.nome.toLowerCase().includes(search.toLowerCase()) || a.n_escola.includes(search) - ), - })) - .filter(t => t.alunos.length > 0); + useEffect(() => { fetchAlunos(); }, []); + + // Abre automaticamente se vier de outra página com params + useEffect(() => { + if (params.alunoId && typeof params.alunoId === 'string' && !modalVisible) { + abrirSumarios({ id: params.alunoId, nome: (params.nome as string) || 'Aluno', n_escola: '', turma: '' }); + } + }, [params.alunoId]); + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchAlunos(); + }, []); + + const filteredTurmas = useMemo(() => { + return turmas + .map(turma => ({ + ...turma, + alunos: turma.alunos.filter(a => + a.nome.toLowerCase().includes(search.toLowerCase()) || a.n_escola.includes(search) + ), + })) + .filter(t => t.alunos.length > 0); + }, [turmas, search]); return ( - - - - - + + + + + {/* HEADER */} + router.back()} - style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.btnAction, { borderColor: cores.borda }]} + onPress={() => router.back()} > - + - + + Sumários - Estágio Atual + Caderno de Registos - + + + + - - - + {/* SEARCH */} + + + + + - - {loading ? ( - - - - ) : ( - item.nome} - contentContainerStyle={styles.listContent} - showsVerticalScrollIndicator={false} - renderItem={({ item }) => ( - - - - {item.nome} - + {loading && !refreshing ? ( + + ) : ( + item.nome} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + refreshControl={} + renderItem={({ item }) => ( + + + + {item.nome} + + - {item.alunos.map(aluno => ( - abrirSumarios(aluno)} - > - - {aluno.nome.charAt(0).toUpperCase()} - - - {aluno.nome} - Nº {aluno.n_escola} - - - - - - ))} - - )} - /> - )} - - setModalVisible(false)}> - - - - - Caderno de Sumários - {alunoSelecionado?.nome} - - setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> - - - - - {loadingSumarios ? ( - - ) : ( - - {sumarios.length === 0 ? ( - - - + {item.alunos.map((aluno) => ( + abrirSumarios(aluno)} + > + + + {aluno.nome.charAt(0).toUpperCase()} + - Sem sumários neste estágio - Não foram encontrados registos para o período atual. - - ) : ( - sumarios.map(s => ( - - - - - {new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })} - - - + + + {aluno.nome} + + + Nº {aluno.n_escola} • Ver Caderno - + + + + + + + ))} + + )} + /> + )} + + {/* MODAL DE SUMÁRIOS */} + setModalVisible(false)}> + + + + + Caderno de Sumários + {alunoSelecionado?.nome} + + setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> + + + + + {loadingSumarios ? ( + + ) : ( + + {sumarios.length === 0 ? ( + + + Sem sumários registados. + + ) : ( + sumarios.map((s) => ( + + + + + + {new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })} + + + + {s.sumario} - - )) - )} - - )} + )) + )} + + )} + - - - + + + + ); }); -export default SumariosAlunos; - const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 }, - headerContainer: { paddingHorizontal: 24, paddingBottom: 20, paddingTop: 10 }, - topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - btnBack: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - titleWrapper: { alignItems: 'center' }, - headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 }, - searchWrapper: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 18, paddingHorizontal: 16, height: 54 }, - inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' }, - listContent: { paddingHorizontal: 24, paddingBottom: 40 }, - turmaSection: { marginBottom: 30 }, - turmaHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, paddingLeft: 4 }, - turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 }, - turmaTitle: { fontSize: 16, fontWeight: '800' }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 24, marginBottom: 12, elevation: 4, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 1, shadowRadius: 12 }, - avatarBox: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - avatarChar: { fontSize: 20, fontWeight: '900' }, - alunoInfo: { flex: 1, marginLeft: 16 }, - alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 }, - alunoSub: { fontSize: 13, fontWeight: '500' }, - goCircle: { width: 32, height: 32, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, - loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'flex-end' }, - modalContent: { height: '88%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, elevation: 2 }, - modalTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, - modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 }, - closeBtn: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - modalScroll: { padding: 20, paddingBottom: 50 }, - sumarioCard: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 1, shadowRadius: 6 }, - sumarioHeaderRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 }, - dateBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 }, - dateText: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase' }, - sumarioLine: { flex: 1, height: 1, backgroundColor: 'rgba(128,128,128,0.1)', marginLeft: 15 }, - sumarioBody: { paddingLeft: 4 }, - sumarioTexto: { fontSize: 15, lineHeight: 24, fontWeight: '500', letterSpacing: 0.2 }, - emptyContainer: { alignItems: 'center', marginTop: 100 }, - emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - emptyText: { fontSize: 16, fontWeight: '700' }, - emptySub: { fontSize: 13, textAlign: 'center', marginTop: 8, opacity: 0.7, paddingHorizontal: 30 } -}); \ No newline at end of file + safe: { flex: 1 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + searchSection: { paddingHorizontal: 24, marginBottom: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, + searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + listPadding: { paddingHorizontal: 24, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18, marginTop: 10 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 18, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 15 }, + alunoNome: { fontSize: 16, fontWeight: '800' }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, + idText: { fontSize: 12, fontWeight: '600' }, + statusBadge: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, + modalContent: { height: '85%', borderTopLeftRadius: 40, borderTopRightRadius: 40 }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 24 }, + modalTitle: { fontSize: 20, fontWeight: '900' }, + modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase' }, + closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + sumarioCard: { padding: 20, borderRadius: 24, marginBottom: 16, borderWidth: 1 }, + sumarioTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, + dateTag: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 10 }, + dateTagText: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase' }, + dotLine: { flex: 1, height: 1, marginLeft: 12, opacity: 0.3 }, + sumarioTexto: { fontSize: 15, lineHeight: 22, fontWeight: '600' }, + emptyContainer: { marginTop: 80, alignItems: 'center' }, +}); + +export default SumariosAlunos; \ No newline at end of file