finalizar design
This commit is contained in:
@@ -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;
|
||||
@@ -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 }]}>Nº 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 }]}>Nº {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;
|
||||
@@ -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;
|
||||
@@ -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 }]}>Nº {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 }]}>Nº {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;
|
||||
Reference in New Issue
Block a user