finalizar design

This commit is contained in:
2026-04-13 15:01:24 +01:00
parent f61baaf134
commit 83095f5a2d
4 changed files with 775 additions and 735 deletions

View File

@@ -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<Presenca[]>([]);
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 (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={styles.headerArea}>
<View style={styles.topRow}>
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={20} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={styles.titleWrapper}>
<View style={{ alignItems: 'center', flex: 1 }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Histórico</Text>
<Text style={[styles.headerSubtitle, { color: cores.azul }]} numberOfLines={1}>{nomeStr}</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]} numberOfLines={1}>{nomeStr}</Text>
</View>
<TouchableOpacity
onPress={fetchHistorico}
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={fetchHistorico}
>
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
</View>
</View>
{loading ? (
<View style={styles.centered}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{presencas.length === 0 ? (
<View style={styles.emptyContainer}>
<View style={[styles.emptyIconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="calendar-clear-outline" size={40} color={cores.azul} />
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 50 }} />
) : (
<FlatList
data={presencas}
keyExtractor={(item) => item.id}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
ListHeaderComponent={() => (
<View style={styles.sectionHeader}>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Registos Recentes</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem registos neste estágio</Text>
<Text style={[styles.emptySub, { color: cores.secundario }]}>Não foram encontradas presenças ou faltas para o período selecionado.</Text>
</View>
) : (
<View style={styles.timelineWrapper}>
<View style={[styles.timelineLine, { backgroundColor: cores.linha }]} />
{presencas.map((item) => {
const isPresente = item.estado === 'presente';
const dataObj = new Date(item.data);
return (
<View key={item.id} style={styles.timelineItem}>
<View style={[styles.timelineDot, { backgroundColor: isPresente ? cores.verde : cores.vermelho, borderColor: cores.fundo }]} />
<TouchableOpacity
activeOpacity={0.8}
onPress={() => handleNavigation(item)}
style={[styles.modernCard, { backgroundColor: cores.card }]}
>
<View style={styles.cardMain}>
<View style={styles.dateInfo}>
<Text style={[styles.dayText, { color: cores.texto }]}>
{dataObj.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
</Text>
<Text style={[styles.yearText, { color: cores.secundario }]}>
{dataObj.getFullYear()}
</Text>
</View>
<View style={styles.dividerVertical} />
<View style={styles.contentInfo}>
<View style={styles.statusRow}>
<Text style={[styles.statusText, { color: isPresente ? cores.azul : cores.vermelho }]}>
{isPresente ? 'PRESENÇA MARCADA' : 'FALTA REGISTADA'}
</Text>
{item.lat && item.lng && (
<TouchableOpacity onPress={() => abrirMapa(item.lat!, item.lng!)} style={styles.mapSmallBtn}>
<Ionicons name="location" size={14} color={cores.azul} />
</TouchableOpacity>
)}
</View>
<Text style={[styles.infoHint, { color: cores.secundario }]}>
Clique para ver {isPresente ? 'o sumário' : 'a justificação'}
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</View>
</TouchableOpacity>
)}
renderItem={({ item }) => {
const isPresente = item.estado === 'presente';
const dataObj = new Date(item.data);
return (
<TouchableOpacity
activeOpacity={0.8}
onPress={() => handleNavigation(item)}
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<View style={[styles.dateBox, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.dateDay, { color: cores.azul }]}>
{dataObj.getDate()}
</Text>
<Text style={[styles.dateMonth, { color: cores.azul }]}>
{dataObj.toLocaleDateString('pt-PT', { month: 'short' }).replace('.', '')}
</Text>
</View>
);
})}
</View>
)}
</ScrollView>
)}
</SafeAreaView>
<View style={styles.infoArea}>
<View style={styles.statusRow}>
<View style={[styles.indicator, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
<Text style={[styles.statusText, { color: cores.texto }]}>
{isPresente ? 'Presença Registada' : 'Falta Marcada'}
</Text>
</View>
<Text style={[styles.subInfo, { color: cores.secundario }]}>
{isPresente ? 'Ver sumário do dia' : 'Ver justificação'}
</Text>
</View>
<View style={styles.actionIcons}>
{item.lat && item.lng && (
<TouchableOpacity
onPress={() => abrirMapa(item.lat!, item.lng!)}
style={[styles.iconBtn, { backgroundColor: cores.azulSuave }]}
>
<Ionicons name="location" size={16} color={cores.azul} />
</TouchableOpacity>
)}
<Ionicons name="chevron-forward" size={18} color={cores.borda} />
</View>
</TouchableOpacity>
);
}}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="calendar-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Sem registos este ano.</Text>
</View>
)}
/>
)}
</SafeAreaView>
</View>
);
}
});
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 },
});
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;

View File

@@ -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<Aluno | null>(null);
const [faltas, setFaltas] = useState<Falta[]>([]);
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<string, Aluno[]> = {};
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 (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.headerFixed}>
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Faltas</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Histórico de Assiduidade</Text>
</View>
<TouchableOpacity
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={fetchAlunos}
>
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>Gestão de Faltas</Text>
<View style={{ width: 42 }} />
</View>
<View style={[styles.searchContainer, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
placeholder="Procurar aluno ou nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
style={[styles.searchInput, { color: cores.texto }]}
/>
{/* SEARCH */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar aluno ou nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
</View>
{loading ? (
<View style={styles.centered}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.section}>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>{item.nome}</Text>
{item.alunos.map(aluno => (
<TouchableOpacity key={aluno.id} activeOpacity={0.7} style={[styles.card, { backgroundColor: cores.card }]} onPress={() => abrirFaltas(aluno)}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Text style={styles.avatarText}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.info}>
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.subText, { color: cores.secundario }]}> Aluno: {aluno.n_escola}</Text>
</View>
<View style={[styles.alertIcon, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="alert-circle" size={18} color={cores.vermelho} />
</View>
</TouchableOpacity>
))}
</View>
)}
/>
)}
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 50 }} />
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderItem={({ item }) => (
<View style={{ marginBottom: 25 }}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
<Modal visible={modalFaltasVisible} animationType="slide" transparent onRequestClose={() => setModalFaltasVisible(false)}>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={[styles.modalHeader, { backgroundColor: cores.card }]}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Faltas de Estágio</Text>
<Text style={[styles.modalSubtitle, { color: cores.azul }]}>{alunoSelecionado?.nome}</Text>
</View>
<TouchableOpacity onPress={() => setModalFaltasVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
</View>
{loadingFaltas ? (
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
) : (
<ScrollView contentContainerStyle={{ padding: 20 }} showsVerticalScrollIndicator={false}>
{faltas.length === 0 ? (
<View style={styles.emptyState}>
<View style={[styles.emptyIconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="checkmark-done" size={40} color={cores.azul} />
{item.alunos.map((aluno) => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.8}
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => abrirFaltas(aluno)}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>
{aluno.nome.charAt(0).toUpperCase()}
</Text>
</View>
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem faltas no período atual</Text>
</View>
) : (
faltas.map(f => (
<View key={f.id} style={[styles.faltaCard, { backgroundColor: cores.card }]}>
<View style={styles.faltaRow}>
<View style={[styles.dateBadge, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="calendar" size={18} color={cores.azul} />
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<View style={styles.idRow}>
<Ionicons name="finger-print-outline" size={13} color={cores.secundario} />
<Text style={[styles.idText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
</View>
<View style={[styles.statusBadge, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="warning-outline" size={12} color={cores.vermelho} />
<Text style={[styles.statusText, { color: cores.vermelho }]}>VER FALTAS</Text>
</View>
</TouchableOpacity>
))}
</View>
)}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="people-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Nenhum aluno encontrado.</Text>
</View>
)}
/>
)}
{/* MODAL DE DETALHES DE FALTAS */}
<Modal visible={modalVisible} animationType="slide" transparent onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={[styles.modalHeader, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Registo de Faltas</Text>
<Text style={[styles.modalSubtitle, { color: cores.laranja }]}>{alunoSelecionado?.nome}</Text>
</View>
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
</View>
{loadingFaltas ? (
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
) : (
<ScrollView contentContainerStyle={{ padding: 24 }} showsVerticalScrollIndicator={false}>
{faltas.length === 0 ? (
<View style={styles.emptyContainer}>
<Ionicons name="happy-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Este aluno não tem faltas no estágio atual!</Text>
</View>
) : (
faltas.map((f) => (
<View key={f.id} style={[styles.faltaCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.dateIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="calendar-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.faltaData, { color: cores.texto }]}>
{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' })}
</Text>
<Text style={[styles.faltaStatus, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
{f.justificacao_url ? 'Justificada' : 'Injustificada'}
<Text style={[styles.statusFaltaText, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
{f.justificacao_url ? 'JUSTIFICADA' : 'INJUSTIFICADA'}
</Text>
</View>
{f.justificacao_url && (
<TouchableOpacity style={[styles.viewBtn, { backgroundColor: cores.azul }]} onPress={() => verDocumento(f.justificacao_url!)}>
<TouchableOpacity style={[styles.btnView, { backgroundColor: cores.azul }]} onPress={() => verDocumento(f.justificacao_url!)}>
<Ionicons name="eye-outline" size={18} color="#fff" />
</TouchableOpacity>
)}
</View>
</View>
))
)}
</ScrollView>
)}
))
)}
</ScrollView>
)}
</View>
</View>
</View>
</Modal>
</SafeAreaView>
</Modal>
</SafeAreaView>
</View>
);
});
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' }
});
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;

View File

@@ -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<string, Aluno[]> = {};
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 (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* HEADER DINÂMICO */}
<View style={styles.headerContainer}>
<View style={styles.topRow}>
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER EPVC */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={20} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={styles.titleWrapper}>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Presenças</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>Lista de Alunos</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Controlo Diário</Text>
</View>
<View style={{ width: 45 }} />
<TouchableOpacity
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={fetchAlunos}
>
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
</View>
<View style={[styles.searchWrapper, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={18} color={cores.azul} />
<TextInput
placeholder="Procurar aluno ou Nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
style={[styles.inputSearch, { color: cores.texto }]}
/>
{search !== '' && (
<TouchableOpacity onPress={() => setSearch('')}>
<Ionicons name="close-circle" size={18} color={cores.secundario} />
</TouchableOpacity>
)}
{/* SEARCH BAR */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Procurar aluno..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
</View>
{loading ? (
<View style={styles.loaderArea}>
<ActivityIndicator size="large" color={cores.azul} />
<Text style={[styles.loaderText, { color: cores.secundario }]}>A carregar turma...</Text>
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.turmaSection}>
<View style={styles.turmaHeader}>
<View style={[styles.turmaDot, { backgroundColor: cores.azul }]} />
<Text style={[styles.turmaTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.countBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.countText, { color: cores.azul }]}>{item.alunos.length}</Text>
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 50 }} />
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderItem={({ item }) => (
<View style={{ marginBottom: 25 }}>
{/* SEPARADOR DE TURMA */}
<View style={styles.sectionHeader}>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
</View>
{item.alunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.7}
style={[styles.alunoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}
onPress={() =>
router.push({
pathname: '/Professor/Alunos/CalendarioPresencas',
params: { alunoId: aluno.id, nome: aluno.nome },
})
}
>
<View style={[styles.avatarBox, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarChar, { color: cores.azul }]}>
{aluno.nome.charAt(0).toUpperCase()}
</Text>
</View>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{aluno.nome}</Text>
<View style={styles.alunoStatusRow}>
<Ionicons name="calendar-outline" size={12} color={cores.secundario} />
<Text style={[styles.alunoSub, { color: cores.secundario }]}> Registo de faltas</Text>
{item.alunos.map((aluno) => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.8}
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() =>
router.push({
pathname: '/Professor/Alunos/CalendarioPresencas',
params: { alunoId: aluno.id, nome: aluno.nome },
})
}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>
{aluno.nome.charAt(0).toUpperCase()}
</Text>
</View>
</View>
<View style={[styles.goCircle, { backgroundColor: cores.fundo }]}>
<Ionicons name="chevron-forward" size={16} color={cores.azul} />
</View>
</TouchableOpacity>
))}
</View>
)}
/>
)}
</SafeAreaView>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<View style={styles.idRow}>
<Ionicons name="calendar-outline" size={13} color={cores.secundario} />
<Text style={[styles.idText, { color: cores.secundario }]}>Abrir Calendário</Text>
</View>
</View>
<View style={[styles.statusBadge, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="chevron-forward" size={16} color={cores.azul} />
</View>
</TouchableOpacity>
))}
</View>
)}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="people-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Sem resultados.</Text>
</View>
)}
/>
)}
</SafeAreaView>
</View>
);
});
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' },
});
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;

View File

@@ -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<Aluno | null>(null);
const [sumarios, setSumarios] = useState<Sumario[]>([]);
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<string, Aluno[]> = {};
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 (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={styles.headerContainer}>
<View style={styles.topRow}>
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={20} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={styles.titleWrapper}>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Sumários</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>Estágio Atual</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Caderno de Registos</Text>
</View>
<View style={{ width: 45 }} />
<TouchableOpacity
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={fetchAlunos}
>
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
</View>
<View style={[styles.searchWrapper, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={18} color={cores.azul} />
<TextInput
placeholder="Procurar aluno..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
style={[styles.inputSearch, { color: cores.texto }]}
/>
{/* SEARCH */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar aluno..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
</View>
{loading ? (
<View style={styles.loaderArea}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.turmaSection}>
<View style={styles.turmaHeader}>
<View style={[styles.turmaDot, { backgroundColor: cores.azul }]} />
<Text style={[styles.turmaTitle, { color: cores.texto }]}>{item.nome}</Text>
</View>
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 50 }} />
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderItem={({ item }) => (
<View style={{ marginBottom: 25 }}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
{item.alunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.7}
style={[styles.alunoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}
onPress={() => abrirSumarios(aluno)}
>
<View style={[styles.avatarBox, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarChar, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.alunoSub, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
<View style={[styles.goCircle, { backgroundColor: cores.fundo }]}>
<Ionicons name="reader-outline" size={16} color={cores.azul} />
</View>
</TouchableOpacity>
))}
</View>
)}
/>
)}
<Modal visible={modalVisible} animationType="slide" transparent={true} onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={[styles.modalHeader, { backgroundColor: cores.card }]}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Caderno de Sumários</Text>
<Text style={[styles.modalSubtitle, { color: cores.azul }]}>{alunoSelecionado?.nome}</Text>
</View>
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
</View>
{loadingSumarios ? (
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
) : (
<ScrollView contentContainerStyle={styles.modalScroll} showsVerticalScrollIndicator={false}>
{sumarios.length === 0 ? (
<View style={styles.emptyContainer}>
<View style={[styles.emptyIconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="receipt-outline" size={40} color={cores.azul} />
{item.alunos.map((aluno) => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.8}
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => abrirSumarios(aluno)}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>
{aluno.nome.charAt(0).toUpperCase()}
</Text>
</View>
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem sumários neste estágio</Text>
<Text style={[styles.emptySub, { color: cores.secundario }]}>Não foram encontrados registos para o período atual.</Text>
</View>
) : (
sumarios.map(s => (
<View key={s.id} style={[styles.sumarioCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}>
<View style={styles.sumarioHeaderRow}>
<View style={[styles.dateBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.dateText, { color: cores.azul }]}>
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
</Text>
</View>
<View style={styles.sumarioLine} />
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<View style={styles.idRow}>
<Ionicons name="book-outline" size={13} color={cores.secundario} />
<Text style={[styles.idText, { color: cores.secundario }]}> {aluno.n_escola} Ver Caderno</Text>
</View>
<View style={styles.sumarioBody}>
</View>
<View style={[styles.statusBadge, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
</View>
</TouchableOpacity>
))}
</View>
)}
/>
)}
{/* MODAL DE SUMÁRIOS */}
<Modal visible={modalVisible} animationType="slide" transparent onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={[styles.modalHeader, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Caderno de Sumários</Text>
<Text style={[styles.modalSubtitle, { color: cores.laranja }]}>{alunoSelecionado?.nome}</Text>
</View>
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
</View>
{loadingSumarios ? (
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
) : (
<ScrollView contentContainerStyle={{ padding: 24, paddingBottom: 60 }} showsVerticalScrollIndicator={false}>
{sumarios.length === 0 ? (
<View style={styles.emptyContainer}>
<Ionicons name="document-text-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Sem sumários registados.</Text>
</View>
) : (
sumarios.map((s) => (
<View key={s.id} style={[styles.sumarioCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.sumarioTop}>
<View style={[styles.dateTag, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="calendar-outline" size={12} color={cores.azul} />
<Text style={[styles.dateTagText, { color: cores.azul }]}>
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
</Text>
</View>
<View style={[styles.dotLine, { backgroundColor: cores.borda }]} />
</View>
<Text style={[styles.sumarioTexto, { color: cores.texto }]}>{s.sumario}</Text>
</View>
</View>
))
)}
</ScrollView>
)}
))
)}
</ScrollView>
)}
</View>
</View>
</View>
</Modal>
</SafeAreaView>
</Modal>
</SafeAreaView>
</View>
);
});
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 }
});
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;