alunoHome & empresaHome
This commit is contained in:
307
app/Empresa/EmpresaHome.tsx
Normal file
307
app/Empresa/EmpresaHome.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function EmpresaHome() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const [pendentes, setPendentes] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [empresaNome, setEmpresaNome] = useState('');
|
||||
|
||||
// Estados do Toast Animado
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const themeStyles = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
azulSuave: '#00c3ff',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: 20, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3000);
|
||||
});
|
||||
}, [slideAnim]);
|
||||
|
||||
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
// 1. Identificar a empresa logada
|
||||
const { data: empresa, error: empError } = await supabase
|
||||
.from('empresas')
|
||||
.select('id, nome')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (empError || !empresa) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
setEmpresaNome(empresa.nome);
|
||||
|
||||
// 2. Buscar alunos vinculados (estágios)
|
||||
const { data: estagios } = await supabase
|
||||
.from('estagios')
|
||||
.select('aluno_id')
|
||||
.eq('empresa_id', empresa.id);
|
||||
|
||||
if (!estagios || estagios.length === 0) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const alunoIds = estagios.map(e => e.aluno_id);
|
||||
|
||||
// 3. Buscar nomes dos alunos
|
||||
const { data: alunos } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome')
|
||||
.in('id', alunoIds);
|
||||
|
||||
const mapaAlunos: Record<string, string> = {};
|
||||
alunos?.forEach(a => { mapaAlunos[a.id] = a.nome; });
|
||||
|
||||
// 4. Buscar apenas PRESENÇAS pendentes de validação
|
||||
// IMPORTANTE: Faltas não aparecem aqui, vão direto para o professor.
|
||||
const { data: presencas } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.in('aluno_id', alunoIds)
|
||||
.eq('estado', 'presente')
|
||||
.eq('estado_tutor', 'pendente')
|
||||
.order('data', { ascending: false });
|
||||
|
||||
const listaFormatada = presencas?.map(p => ({
|
||||
...p,
|
||||
aluno_nome: mapaAlunos[p.aluno_id] || 'Aluno Desconhecido'
|
||||
})) || [];
|
||||
|
||||
setPendentes(listaFormatada);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast("Falha ao carregar validações.", "error");
|
||||
} finally {
|
||||
if (!isManualRefresh) setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchValidaçõesPendentes(true);
|
||||
}, []);
|
||||
|
||||
const lidarComPresenca = async (id: string, decisao: 'aprovado' | 'rejeitado') => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('presencas')
|
||||
.update({ estado_tutor: decisao })
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
showToast(decisao === 'aprovado' ? "Registo aprovado!" : "Registo rejeitado.", decisao === 'aprovado' ? 'success' : 'info');
|
||||
setPendentes(prev => prev.filter(p => p.id !== id));
|
||||
|
||||
} catch (e: any) {
|
||||
showToast("Erro ao processar validação.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const formatarData = (dataStr: string) => {
|
||||
if (!dataStr) return '';
|
||||
const parts = dataStr.split('-');
|
||||
return parts.length !== 3 ? dataStr : `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* TOAST ANIMADO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: themeStyles.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: themeStyles.verde } :
|
||||
{ backgroundColor: themeStyles.azul }
|
||||
]}>
|
||||
<Ionicons name={toast.type === 'error' ? "warning" : "checkmark-circle"} size={22} color="#FFF" />
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.greeting, { color: themeStyles.secundario }]}>Bem-vindo,</Text>
|
||||
<Text style={[styles.title, { color: themeStyles.texto }]} numberOfLines={1}>
|
||||
{empresaNome || 'Entidade'}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.logoutBtn, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}
|
||||
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
|
||||
>
|
||||
<Ionicons name="log-out-outline" size={22} color={themeStyles.vermelho} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
|
||||
<View style={[styles.countBadge, { backgroundColor: themeStyles.laranja }]}>
|
||||
<Text style={styles.countText}>{pendentes.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading && !refreshing ? (
|
||||
<View style={styles.centerBox}>
|
||||
<ActivityIndicator size="large" color={themeStyles.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={themeStyles.azul} />}
|
||||
>
|
||||
{pendentes.length === 0 ? (
|
||||
<View style={[styles.emptyBox, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Ionicons name="shield-checkmark-outline" size={40} color={themeStyles.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Tudo em ordem!</Text>
|
||||
<Text style={[styles.emptyDesc, { color: themeStyles.secundario }]}>
|
||||
Não existem registos pendentes de validação. **Vai dar merda** se os alunos não trabalharem! 😂
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
pendentes.map((item) => (
|
||||
<View key={item.id} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.cardTop}>
|
||||
<View style={[styles.avatar, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: themeStyles.azul }]}>{item.aluno_nome.charAt(0)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 12 }}>
|
||||
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>{item.aluno_nome}</Text>
|
||||
<View style={styles.dataRow}>
|
||||
<Ionicons name="calendar-outline" size={14} color={themeStyles.laranja} />
|
||||
<Text style={[styles.dataText, { color: themeStyles.secundario }]}>{formatarData(item.data)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.statusTag, { backgroundColor: themeStyles.laranja + '15' }]}>
|
||||
<Text style={[styles.statusTagText, { color: themeStyles.laranja }]}>PENDENTE</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.sumarioBox, { backgroundColor: themeStyles.fundo }]}>
|
||||
<Text style={[styles.sumarioLabel, { color: themeStyles.secundario }]}>SUMÁRIO DO DIA:</Text>
|
||||
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
|
||||
{item.sumario || "O aluno não descreveu as atividades deste dia."}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]}
|
||||
onPress={() => lidarComPresenca(item.id, 'rejeitado')}
|
||||
>
|
||||
<Ionicons name="close-circle-outline" size={20} color={themeStyles.vermelho} />
|
||||
<Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.verde }]}
|
||||
onPress={() => lidarComPresenca(item.id, 'aprovado')}
|
||||
>
|
||||
<Ionicons name="checkmark-circle-outline" size={20} color="#fff" />
|
||||
<Text style={[styles.btnActionText, { color: '#fff' }]}>Aprovar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12 },
|
||||
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 20 },
|
||||
greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
title: { fontSize: 24, fontWeight: '900', marginTop: 2 },
|
||||
logoutBtn: { width: 48, height: 48, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 15, gap: 10 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
countBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10 },
|
||||
countText: { color: '#fff', fontSize: 12, fontWeight: '900' },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
emptyBox: { alignItems: 'center', padding: 40, borderRadius: 28, borderWidth: 1, borderStyle: 'dashed', marginTop: 20 },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
emptyTitle: { fontSize: 20, fontWeight: '900', marginBottom: 8 },
|
||||
emptyDesc: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '600', opacity: 0.8 },
|
||||
|
||||
card: { padding: 20, borderRadius: 28, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 10 },
|
||||
cardTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 },
|
||||
avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, fontWeight: '900' },
|
||||
alunoName: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
dataRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 },
|
||||
dataText: { fontSize: 13, fontWeight: '700' },
|
||||
statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8 },
|
||||
statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
|
||||
|
||||
sumarioBox: { padding: 16, borderRadius: 18, marginBottom: 18 },
|
||||
sumarioLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, letterSpacing: 0.5 },
|
||||
sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 22 },
|
||||
|
||||
actionRow: { flexDirection: 'row', gap: 12 },
|
||||
btnAction: { flex: 1, flexDirection: 'row', height: 52, borderRadius: 16, justifyContent: 'center', alignItems: 'center', gap: 8 },
|
||||
btnActionText: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }
|
||||
});
|
||||
@@ -1,274 +0,0 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function EmpresaHome() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const [pendentes, setPendentes] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [empresaNome, setEmpresaNome] = useState('');
|
||||
|
||||
const themeStyles = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: '#2390a6',
|
||||
laranja: '#dd8707',
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
|
||||
inputFundo: isDarkMode ? '#252525' : '#F1F5F9',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
// 1. Identificar quem é a empresa que tem o login feito
|
||||
const { data: empresa } = await supabase
|
||||
.from('empresas')
|
||||
.select('id, nome')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (!empresa) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
setEmpresaNome(empresa.nome);
|
||||
|
||||
// 2. Buscar todos os estágios ligados a esta empresa
|
||||
const { data: estagios } = await supabase
|
||||
.from('estagios')
|
||||
.select('aluno_id')
|
||||
.eq('empresa_id', empresa.id);
|
||||
|
||||
if (!estagios || estagios.length === 0) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const alunoIds = estagios.map(e => e.aluno_id);
|
||||
|
||||
// 3. Buscar os nomes dos alunos (para o tutor saber quem está a avaliar)
|
||||
const { data: alunos } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome')
|
||||
.in('id', alunoIds);
|
||||
|
||||
const mapaAlunos: Record<string, string> = {};
|
||||
alunos?.forEach(a => { mapaAlunos[a.id] = a.nome; });
|
||||
|
||||
// 4. Buscar apenas as presenças que estão PENDENTES para estes alunos
|
||||
const { data: presencas } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.in('aluno_id', alunoIds)
|
||||
.eq('estado', 'presente')
|
||||
.eq('estado_tutor', 'pendente')
|
||||
.order('data', { ascending: false });
|
||||
|
||||
const listaFormatada = presencas?.map(p => ({
|
||||
...p,
|
||||
aluno_nome: mapaAlunos[p.aluno_id] || 'Aluno Desconhecido'
|
||||
})) || [];
|
||||
|
||||
setPendentes(listaFormatada);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Alert.alert("Erro", "Falha ao carregar as validações pendentes.");
|
||||
} finally {
|
||||
if (!isManualRefresh) setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Atualiza sempre que o ecrã ganha foco
|
||||
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchValidaçõesPendentes(true);
|
||||
}, []);
|
||||
|
||||
// 🟢 FUNÇÃO PARA APROVAR OU REJEITAR
|
||||
const lidarComPresenca = async (aluno_id: string, data: string, decisao: 'aprovado' | 'rejeitado') => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('presencas')
|
||||
.update({ estado_tutor: decisao })
|
||||
.match({ aluno_id, data }); // Dá match exato ao aluno e àquele dia
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Avisa visualmente do sucesso e limpa aquele cartão da lista
|
||||
if (decisao === 'aprovado') {
|
||||
Alert.alert("✅ Validado", "Horas e sumário aprovados com sucesso!");
|
||||
} else {
|
||||
Alert.alert("❌ Rejeitado", "O registo foi devolvido ao aluno para correção.");
|
||||
}
|
||||
|
||||
// Atualiza a lista removendo o que acabou de ser processado
|
||||
setPendentes(prev => prev.filter(p => !(p.aluno_id === aluno_id && p.data === data)));
|
||||
|
||||
} catch (e: any) {
|
||||
Alert.alert("Erro ao validar", e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Formatar data (AAAA-MM-DD -> DD/MM/AAAA)
|
||||
const formatarData = (dataStr: string) => {
|
||||
if (!dataStr) return '';
|
||||
const parts = dataStr.split('-');
|
||||
if (parts.length !== 3) return dataStr;
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.topBar}>
|
||||
<View>
|
||||
<Text style={[styles.greeting, { color: themeStyles.textoSecundario }]}>Painel da Entidade</Text>
|
||||
<Text style={[styles.title, { color: themeStyles.texto }]}>{empresaNome || 'A carregar...'}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.logoutBtn, { borderColor: themeStyles.borda }]} onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}>
|
||||
<Ionicons name="log-out-outline" size={24} color={themeStyles.vermelho} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
|
||||
<View style={[styles.badge, { backgroundColor: themeStyles.laranja + '20' }]}>
|
||||
<Text style={[styles.badgeText, { color: themeStyles.laranja }]}>{pendentes.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading && !refreshing ? (
|
||||
<View style={styles.centerBox}>
|
||||
<ActivityIndicator size="large" color={themeStyles.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />}
|
||||
>
|
||||
{pendentes.length === 0 ? (
|
||||
<View style={[styles.emptyBox, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}>
|
||||
<Ionicons name="checkmark-done-circle" size={60} color={themeStyles.verde} style={{ marginBottom: 15 }} />
|
||||
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Tudo em dia!</Text>
|
||||
<Text style={[styles.emptyDesc, { color: themeStyles.textoSecundario }]}>
|
||||
Não tens sumários ou presenças de alunos a aguardar a tua validação neste momento.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
pendentes.map((item, index) => (
|
||||
<View key={index} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>
|
||||
<Ionicons name="person" size={14} color={themeStyles.textoSecundario} /> {item.aluno_nome}
|
||||
</Text>
|
||||
<Text style={[styles.dataText, { color: themeStyles.azul }]}>
|
||||
<Ionicons name="calendar-outline" size={12} /> {formatarData(item.data)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.statusTag, { backgroundColor: themeStyles.laranja + '20' }]}>
|
||||
<Text style={[styles.statusTagText, { color: themeStyles.laranja }]}>POR VALIDAR</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.sumarioBox, { backgroundColor: themeStyles.inputFundo }]}>
|
||||
<Text style={[styles.sumarioLabel, { color: themeStyles.textoSecundario }]}>Sumário Submetido:</Text>
|
||||
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
|
||||
{item.sumario ? item.sumario : "O aluno não escreveu sumário para este dia."}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]}
|
||||
onPress={() => lidarComPresenca(item.aluno_id, item.data, 'rejeitado')}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={themeStyles.vermelho} />
|
||||
<Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.verde }]}
|
||||
onPress={() => lidarComPresenca(item.aluno_id, item.data, 'aprovado')}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color="#fff" />
|
||||
<Text style={[styles.btnActionText, { color: '#fff' }]}>Aprovar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 10 },
|
||||
greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
title: { fontSize: 24, fontWeight: '900', marginTop: 2 },
|
||||
logoutBtn: { width: 45, height: 45, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
headerTitleContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 15, gap: 10 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '800' },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 },
|
||||
badgeText: { fontSize: 12, fontWeight: '900' },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
|
||||
emptyBox: { alignItems: 'center', padding: 40, borderRadius: 24, borderWidth: 1, borderStyle: 'dashed', marginTop: 30 },
|
||||
emptyTitle: { fontSize: 20, fontWeight: '900', marginBottom: 8 },
|
||||
emptyDesc: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '500' },
|
||||
|
||||
card: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 15 },
|
||||
alunoName: { fontSize: 17, fontWeight: '900', marginBottom: 4 },
|
||||
dataText: { fontSize: 13, fontWeight: '800' },
|
||||
statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
|
||||
statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
|
||||
|
||||
sumarioBox: { padding: 15, borderRadius: 16, marginBottom: 15 },
|
||||
sumarioLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6 },
|
||||
sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 20 },
|
||||
|
||||
actionRow: { flexDirection: 'row', gap: 12 },
|
||||
btnAction: { flex: 1, flexDirection: 'row', height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center', gap: 6 },
|
||||
btnActionText: { fontSize: 14, fontWeight: '800' }
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
// app/(Professor)/HistoricoPresencas.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
FlatList,
|
||||
Linking,
|
||||
Platform,
|
||||
@@ -42,25 +42,43 @@ const HistoricoPresencas = memo(() => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// ESTADOS DO TOAST
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const 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 erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
vermelho: '#EF4444',
|
||||
verde: '#10B981',
|
||||
vermelho: erroCor,
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
verde: sucessoCor,
|
||||
verdeSuave: isDarkMode ? 'rgba(16, 185, 129, 0.15)' : '#DCFCE7',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'error') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const fetchHistorico = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -70,7 +88,6 @@ const HistoricoPresencas = memo(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Procurar o estágio deste aluno
|
||||
const { data: estagioData, error: errEstagio } = await supabase
|
||||
.from('estagios')
|
||||
.select('data_inicio, data_fim')
|
||||
@@ -86,7 +103,6 @@ const HistoricoPresencas = memo(() => {
|
||||
|
||||
const estagio = estagioData[0];
|
||||
|
||||
// 2. Buscar presenças no intervalo do estágio
|
||||
const { data: presencasData, error: errPresencas } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
@@ -101,7 +117,7 @@ const HistoricoPresencas = memo(() => {
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Erro Geral:", error);
|
||||
Alert.alert("Erro", "Falha ao carregar dados do estágio.");
|
||||
showToast("Falha ao carregar dados do estágio.", 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -119,10 +135,12 @@ const HistoricoPresencas = memo(() => {
|
||||
|
||||
const handleNavigation = (item: Presenca) => {
|
||||
if (item.estado === 'faltou') {
|
||||
router.push({
|
||||
pathname: '/Professor/Alunos/Faltas',
|
||||
params: { alunoId: idStr, nome: nomeStr }
|
||||
});
|
||||
if (item.justificacao_url) {
|
||||
Linking.openURL(item.justificacao_url);
|
||||
} else {
|
||||
// 🟢 AVISO MODERNO USADO AQUI
|
||||
showToast("Não existe justificação anexada a esta falta.", 'info');
|
||||
}
|
||||
} else {
|
||||
router.push({
|
||||
pathname: '/Professor/Alunos/Sumarios',
|
||||
@@ -143,25 +161,43 @@ const HistoricoPresencas = memo(() => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{ alignItems: 'center', flex: 1 }}>
|
||||
<View style={{ alignItems: 'center', flex: 1, paddingHorizontal: 10 }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Histórico</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]} numberOfLines={1}>{nomeStr}</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]} numberOfLines={1}>
|
||||
{nomeStr}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={fetchHistorico}
|
||||
>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
@@ -174,7 +210,8 @@ const HistoricoPresencas = memo(() => {
|
||||
<FlatList
|
||||
data={presencas}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 40 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
ListHeaderComponent={() => (
|
||||
<View style={styles.sectionHeader}>
|
||||
@@ -187,51 +224,63 @@ const HistoricoPresencas = memo(() => {
|
||||
const isPresente = item.estado === 'presente';
|
||||
const dataObj = new Date(item.data);
|
||||
|
||||
const bgColor = isPresente ? cores.verdeSuave : cores.vermelhoSuave;
|
||||
const mainColor = isPresente ? cores.verde : cores.vermelho;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
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 }]}>
|
||||
<View style={[styles.dateBox, { backgroundColor: bgColor }]}>
|
||||
<Text style={[styles.dateDay, { color: mainColor }]}>
|
||||
{dataObj.getDate()}
|
||||
</Text>
|
||||
<Text style={[styles.dateMonth, { color: cores.azul }]}>
|
||||
<Text style={[styles.dateMonth, { color: mainColor }]}>
|
||||
{dataObj.toLocaleDateString('pt-PT', { month: 'short' }).replace('.', '')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoArea}>
|
||||
<View style={styles.statusRow}>
|
||||
<View style={[styles.indicator, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
|
||||
<View style={[styles.indicator, { backgroundColor: mainColor }]} />
|
||||
<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'}
|
||||
{isPresente
|
||||
? 'Ver sumário inserido'
|
||||
: (item.justificacao_url ? 'Abrir justificação anexada' : 'Falta sem justificação')
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionIcons}>
|
||||
{item.lat && item.lng && (
|
||||
{item.lat && item.lng ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => abrirMapa(item.lat!, item.lng!)}
|
||||
style={[styles.iconBtn, { backgroundColor: cores.azulSuave }]}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<Ionicons name="location" size={16} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{(!isPresente && !item.justificacao_url) ? null : (
|
||||
<Ionicons name={isPresente ? "chevron-forward" : "document-attach"} size={20} color={cores.secundario} />
|
||||
)}
|
||||
<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 style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="calendar-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhum registo efetuado.</Text>
|
||||
<Text style={[styles.emptySubText, { color: cores.secundario, opacity: 0.7 }]}>As presenças/faltas irão aparecer aqui.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
@@ -243,27 +292,40 @@ const HistoricoPresencas = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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 },
|
||||
// 🟢 TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 100, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 10, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, 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 },
|
||||
listPadding: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
|
||||
listPadding: { paddingHorizontal: 20, 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' },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10, shadowOffset: { width: 0, height: 2 } },
|
||||
dateBox: { width: 56, height: 56, 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 },
|
||||
|
||||
infoArea: { flex: 1, marginLeft: 16 },
|
||||
statusRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 4 },
|
||||
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' },
|
||||
|
||||
actionIcons: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingRight: 5 },
|
||||
iconBtn: { width: 34, height: 34, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
emptyText: { fontSize: 15, fontWeight: '700' },
|
||||
emptySubText: { fontSize: 13, fontWeight: '600', marginTop: 4 }
|
||||
});
|
||||
|
||||
export default HistoricoPresencas;
|
||||
@@ -1,342 +0,0 @@
|
||||
// app/Professor/Alunos/CriarAluno.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
// --- FUNÇÕES DE DATA ---
|
||||
const formatarDataParaDB = (dataUI: string) => {
|
||||
if (!dataUI || dataUI.length !== 10) return null;
|
||||
const parts = dataUI.split('-');
|
||||
if (parts.length !== 3) return null;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const aplicarMascaraData = (text: string) => {
|
||||
const cleaned = text.replace(/\D/g, '');
|
||||
let formatted = cleaned;
|
||||
if (cleaned.length > 2 && cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`;
|
||||
} else if (cleaned.length > 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`;
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const calcularIdade = (dataPT: string): string => {
|
||||
if (!dataPT || dataPT.length !== 10) return '';
|
||||
const parts = dataPT.split('-');
|
||||
const dia = parseInt(parts[0], 10);
|
||||
const mes = parseInt(parts[1], 10);
|
||||
const ano = parseInt(parts[2], 10);
|
||||
|
||||
if (ano < 1950 || ano > new Date().getFullYear()) return '';
|
||||
|
||||
const hoje = new Date();
|
||||
const nascimento = new Date(ano, mes - 1, dia);
|
||||
|
||||
if (nascimento.getDate() !== dia) return '';
|
||||
|
||||
let idade = hoje.getFullYear() - nascimento.getFullYear();
|
||||
const m = hoje.getMonth() - nascimento.getMonth();
|
||||
if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) idade--;
|
||||
return idade >= 0 ? idade.toString() : '';
|
||||
};
|
||||
|
||||
const CriarAluno = () => {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// ESTADOS DE LOGIN
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno');
|
||||
|
||||
// ESTADOS DE PERFIL (Utilizador / Tutor)
|
||||
const [nome, setNome] = useState('');
|
||||
const [residencia, setResidencia] = useState('');
|
||||
const [telefone, setTelefone] = useState('');
|
||||
const [dataNascimento, setDataNascimento] = useState(''); // Formato DD-MM-AAAA
|
||||
const [idade, setIdade] = useState('');
|
||||
|
||||
// ESTADOS ESPECÍFICOS (Aluno / Professor)
|
||||
const [ano, setAno] = useState('');
|
||||
const [nEscola, setNEscola] = useState('');
|
||||
const [curso, setCurso] = useState('');
|
||||
|
||||
// CAMPOS PARA EMPRESA
|
||||
const [nomeEmpresa, setNomeEmpresa] = useState('');
|
||||
const [nif, setNif] = useState('');
|
||||
const [setor, setSetor] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const novaIdade = calcularIdade(dataNascimento);
|
||||
if (novaIdade) setIdade(novaIdade);
|
||||
else setIdade('');
|
||||
}, [dataNascimento]);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: '#2390a6',
|
||||
laranja: '#E38E00',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
placeholder: isDarkMode ? '#555' : '#A0AEC0'
|
||||
}), [isDarkMode]);
|
||||
|
||||
const handleCriar = async () => {
|
||||
const emailLimpo = email.trim();
|
||||
|
||||
if (!emailLimpo || !password || !nome) {
|
||||
Alert.alert("Atenção", "Obrigatório: Email, Password e Nome.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tipo === 'empresa' && (!nomeEmpresa || !nif)) {
|
||||
Alert.alert("Atenção", "Nome da Entidade e NIF são obrigatórios para Empresas.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||
email: emailLimpo,
|
||||
password,
|
||||
options: {
|
||||
data: { nome, tipo },
|
||||
emailRedirectTo: undefined
|
||||
}
|
||||
});
|
||||
|
||||
if (authError) throw authError;
|
||||
const user = authData.user;
|
||||
|
||||
if (!user) {
|
||||
Alert.alert("Verificação", "Utilizador criado. Verifique o email para ativar a conta.");
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
const dataFormatadaDB = formatarDataParaDB(dataNascimento);
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.insert([{
|
||||
id: user.id,
|
||||
nome, // É o nome da pessoa (Aluno, Prof, ou Tutor)
|
||||
email: emailLimpo,
|
||||
residencia,
|
||||
telefone,
|
||||
idade: idade ? parseInt(idade) : null,
|
||||
data_nascimento: dataFormatadaDB,
|
||||
tipo,
|
||||
n_escola: tipo === 'aluno' ? nEscola : null, // Apenas para aluno
|
||||
curso: tipo === 'aluno' || tipo === 'professor' ? curso : null
|
||||
}]);
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
if (tipo === 'aluno') {
|
||||
const { error: alunoError } = await supabase
|
||||
.from('alunos')
|
||||
.insert([{
|
||||
id: user.id,
|
||||
nome,
|
||||
n_escola: nEscola,
|
||||
ano: ano ? parseInt(ano) : null,
|
||||
turma_curso: curso.toUpperCase()
|
||||
}]);
|
||||
if (alunoError) throw alunoError;
|
||||
}
|
||||
|
||||
if (tipo === 'empresa') {
|
||||
const { error: empresaError } = await supabase
|
||||
.from('empresas')
|
||||
.insert([{
|
||||
nome: nomeEmpresa,
|
||||
nif: nif,
|
||||
setor: setor,
|
||||
tutor_nome: nome, // O nome do perfil é o nome do tutor
|
||||
tutor_telefone: telefone, // O telefone do perfil é o contacto do tutor
|
||||
user_id: user.id
|
||||
}]);
|
||||
if (empresaError) throw empresaError;
|
||||
}
|
||||
|
||||
Alert.alert("Sucesso", "Novo registo concluído com sucesso!");
|
||||
router.back();
|
||||
|
||||
} catch (err: any) {
|
||||
Alert.alert("Erro ao criar conta", err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="close" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Novo Registo</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={styles.selectorContainer}>
|
||||
{(['aluno', 'professor', 'empresa'] as const).map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
style={[styles.selectorBtn, { backgroundColor: tipo === item ? cores.azul : cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => setTipo(item)}
|
||||
>
|
||||
<Text style={{ color: tipo === item ? '#FFF' : cores.secundario, fontWeight: '900', fontSize: 12 }}>
|
||||
{item.toUpperCase()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<SectionHeader title="Credenciais de Acesso" cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={email} onChangeText={setEmail} placeholder="Email Institucional / Comercial" autoCapitalize="none" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={password} onChangeText={setPassword} secureTextEntry placeholder="Password (mín. 6 caracteres)" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
<SectionHeader title={tipo === 'empresa' ? "Dados do Tutor Responsável" : "Dados Pessoais"} cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? "Nome do Tutor" : "Nome Completo"} placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 2, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={dataNascimento} onChangeText={(t) => setDataNascimento(aplicarMascaraData(t))} placeholder="Nascimento (DD-MM-AAAA)" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<View style={[styles.input, { flex: 1, backgroundColor: cores.card, borderColor: cores.borda, justifyContent: 'center', opacity: 0.7 }]}>
|
||||
<Text style={{ color: cores.texto, textAlign: 'center' }}>{idade ? `${idade} anos` : 'Idade'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={telefone} onChangeText={setTelefone} keyboardType="phone-pad" placeholder="Contacto Telefónico" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={residencia} onChangeText={setResidencia} placeholder={tipo === 'empresa' ? "Morada do Tutor" : "Morada Completa"} placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
{tipo === 'aluno' && (
|
||||
<>
|
||||
<SectionHeader title="Percurso Escolar" cores={cores} />
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={ano} onChangeText={setAno} keyboardType="numeric" placeholder="Ano" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 2, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nEscola} onChangeText={setNEscola} keyboardType="numeric" placeholder="Nº Aluno" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={curso} onChangeText={setCurso} placeholder="Sigla do Curso (ex: GPSI)" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tipo === 'professor' && (
|
||||
<>
|
||||
<SectionHeader title="Docência" cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={curso} onChangeText={setCurso} placeholder="Departamento / Área Especialidade" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tipo === 'empresa' && (
|
||||
<>
|
||||
<SectionHeader title="Dados da Entidade (Empresa)" cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nomeEmpresa} onChangeText={setNomeEmpresa} placeholder="Nome Oficial da Empresa" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nif} onChangeText={setNif} keyboardType="numeric" placeholder="NIF" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={setor} onChangeText={setSetor} placeholder="Setor Atividade" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn, { backgroundColor: cores.azul }]}
|
||||
onPress={handleCriar}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.submitBtnText}>REGISTAR NO SISTEMA</Text>}
|
||||
</TouchableOpacity>
|
||||
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionHeader = ({ title, cores }: any) => (
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{title}</Text>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: { padding: 24, paddingBottom: 80 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15, gap: 15 },
|
||||
backBtn: { width: 45, height: 45, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
sectionTitle: { fontSize: 11, fontWeight: '900', marginTop: 25, marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 },
|
||||
selectorContainer: { flexDirection: 'row', gap: 10, marginBottom: 10 },
|
||||
selectorBtn: { flex: 1, height: 48, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
form: { gap: 10 },
|
||||
input: { height: 55, borderRadius: 16, borderWidth: 1, paddingHorizontal: 18, fontSize: 15, fontWeight: '600' },
|
||||
submitBtn: { height: 62, borderRadius: 20, marginTop: 40, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
submitBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', letterSpacing: 1 },
|
||||
});
|
||||
|
||||
export default CriarAluno;
|
||||
473
app/Professor/Alunos/CriarRegisto.tsx
Normal file
473
app/Professor/Alunos/CriarRegisto.tsx
Normal file
@@ -0,0 +1,473 @@
|
||||
// app/Professor/Alunos/CriarAluno.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
const formatarDataParaDB = (dataUI: string) => {
|
||||
if (!dataUI || dataUI.length !== 10) return null;
|
||||
const parts = dataUI.split('-');
|
||||
if (parts.length !== 3) return null;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const aplicarMascaraData = (text: string) => {
|
||||
const cleaned = text.replace(/\D/g, '');
|
||||
let formatted = cleaned;
|
||||
if (cleaned.length > 2 && cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`;
|
||||
} else if (cleaned.length > 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`;
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const calcularIdade = (dataPT: string): string => {
|
||||
if (!dataPT || dataPT.length !== 10) return '';
|
||||
const parts = dataPT.split('-');
|
||||
const dia = parseInt(parts[0], 10);
|
||||
const mes = parseInt(parts[1], 10);
|
||||
const ano = parseInt(parts[2], 10);
|
||||
|
||||
if (ano < 1950 || ano > new Date().getFullYear()) return '';
|
||||
|
||||
const hoje = new Date();
|
||||
const nascimento = new Date(ano, mes - 1, dia);
|
||||
|
||||
if (nascimento.getDate() !== dia) return '';
|
||||
|
||||
let idade = hoje.getFullYear() - nascimento.getFullYear();
|
||||
const m = hoje.getMonth() - nascimento.getMonth();
|
||||
if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) idade--;
|
||||
return idade >= 0 ? idade.toString() : '';
|
||||
};
|
||||
|
||||
const CriarAluno = () => {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// ESTADOS DO TOAST NOTIFICATION
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
// ESTADOS COMUNS A TODOS
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno');
|
||||
|
||||
// ESTADOS DO PERFIL/TUTOR
|
||||
const [nome, setNome] = useState('');
|
||||
const [residencia, setResidencia] = useState('');
|
||||
const [telefone, setTelefone] = useState('');
|
||||
const [dataNascimento, setDataNascimento] = useState('');
|
||||
const [idade, setIdade] = useState('');
|
||||
|
||||
// ESTADOS ESCOLARES / CURSO
|
||||
const [ano, setAno] = useState('');
|
||||
const [nEscola, setNEscola] = useState('');
|
||||
const [curso, setCurso] = useState('');
|
||||
|
||||
// ESTADOS ESPECÍFICOS EMPRESA
|
||||
const [nomeEmpresa, setNomeEmpresa] = useState('');
|
||||
const [nif, setNif] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const novaIdade = calcularIdade(dataNascimento);
|
||||
if (novaIdade) setIdade(novaIdade);
|
||||
else setIdade('');
|
||||
}, [dataNascimento]);
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design Premium
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
placeholder: isDarkMode ? '#555' : '#94A3B8',
|
||||
verde: sucessoCor,
|
||||
vermelho: erroCor
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = (message: string, type: 'error' | 'success' | 'info' = 'error') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: insets.top + 10,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: -100,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}).start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCriar = async () => {
|
||||
const emailLimpo = email.trim();
|
||||
|
||||
if (!emailLimpo || !password || !nome) {
|
||||
showToast("Preencha todos os campos obrigatórios!", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tipo === 'empresa' && (!nomeEmpresa || !curso)) {
|
||||
showToast("O Nome da Empresa e o Curso são obrigatórios!", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tipo === 'aluno' && (!nEscola || !curso || !ano)) {
|
||||
showToast("Os dados escolares do aluno estão incompletos!", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Criar a conta na Autenticação
|
||||
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||
email: emailLimpo,
|
||||
password,
|
||||
options: {
|
||||
data: { nome, tipo },
|
||||
emailRedirectTo: undefined
|
||||
}
|
||||
});
|
||||
|
||||
if (authError) throw authError;
|
||||
const user = authData.user;
|
||||
|
||||
if (!user) {
|
||||
showToast("Novo utilizador criado!", 'info');
|
||||
setTimeout(() => router.back(), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Criar o Perfil Geral
|
||||
const dataFormatadaDB = tipo !== 'empresa' ? formatarDataParaDB(dataNascimento) : null;
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.insert([{
|
||||
id: user.id,
|
||||
nome,
|
||||
email: emailLimpo,
|
||||
residencia,
|
||||
telefone,
|
||||
idade: (idade && tipo !== 'empresa') ? parseInt(idade) : null,
|
||||
data_nascimento: dataFormatadaDB,
|
||||
tipo,
|
||||
n_escola: tipo === 'aluno' || tipo === 'professor' ? nEscola : null,
|
||||
curso: curso ? curso.toUpperCase() : null
|
||||
}]);
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
// 3. Criar a Entidade Específica
|
||||
if (tipo === 'aluno') {
|
||||
const { error: alunoError } = await supabase
|
||||
.from('alunos')
|
||||
.insert([{
|
||||
profile_id: user.id, // 🟢 CORREÇÃO AQUI: perfil_id igual ao do teu diagrama
|
||||
nome,
|
||||
n_escola: nEscola,
|
||||
ano: ano ? parseInt(ano) : null,
|
||||
turma_curso: curso.toUpperCase()
|
||||
}]);
|
||||
if (alunoError) throw alunoError;
|
||||
}
|
||||
|
||||
if (tipo === 'empresa') {
|
||||
const { error: empresaError } = await supabase
|
||||
.from('empresas')
|
||||
.insert([{
|
||||
nome: nomeEmpresa,
|
||||
nif: nif,
|
||||
morada: residencia,
|
||||
tutor_nome: nome,
|
||||
tutor_telefone: telefone,
|
||||
curso: curso.toUpperCase()
|
||||
// 🟢 CORREÇÃO AQUI: Removi user_id e setor que não existem no diagrama
|
||||
}]);
|
||||
if (empresaError) throw empresaError;
|
||||
}
|
||||
|
||||
showToast("Registo criado com sucesso e adicionado à lista!", 'success');
|
||||
setTimeout(() => router.back(), 1500);
|
||||
|
||||
} catch (err: any) {
|
||||
showToast(err.message || "Erro ao criar o registo.", 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top', 'left', 'right']}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { borderColor: cores.borda, backgroundColor: cores.card }]}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Novo Registo</Text>
|
||||
<Text style={[styles.subtitle, { color: cores.laranja }]}>Sistema Central</Text>
|
||||
</View>
|
||||
<View style={{ width: 45 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={[styles.pillSelector, { backgroundColor: cores.borda }]}>
|
||||
{(['aluno', 'professor', 'empresa'] as const).map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
style={[
|
||||
styles.pillBtn,
|
||||
tipo === item ? { backgroundColor: cores.azul, shadowColor: cores.azul, shadowOpacity: 0.3, shadowRadius: 8, elevation: 4 } : { backgroundColor: 'transparent' }
|
||||
]}
|
||||
onPress={() => setTipo(item)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={{
|
||||
color: tipo === item ? '#FFF' : cores.secundario,
|
||||
fontWeight: tipo === item ? '900' : '700',
|
||||
fontSize: 12,
|
||||
letterSpacing: 0.5
|
||||
}}>
|
||||
{item.toUpperCase()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="key" title="Dados de Acesso (Login)" cores={cores} />
|
||||
<View style={styles.inputGroup}>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={email} onChangeText={setEmail} placeholder="Email" autoCapitalize="none" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={password} onChangeText={setPassword} secureTextEntry placeholder="Password (mín. 6)" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DADOS DA EMPRESA */}
|
||||
{tipo === 'empresa' && (
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="business" title="Dados da Entidade" cores={cores} />
|
||||
<View style={styles.inputGroup}>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nomeEmpresa} onChangeText={setNomeEmpresa} placeholder="Nome da Empresa" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={curso} onChangeText={setCurso} placeholder="Curso Alvo" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nif} onChangeText={setNif} keyboardType="numeric" placeholder="NIF" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* DADOS PESSOAIS / TUTOR */}
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="person" title={tipo === 'empresa' ? 'Dados do Tutor & Localização' : 'Dados Pessoais'} cores={cores} />
|
||||
<View style={styles.inputGroup}>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? 'Nome do Responsável/Tutor' : 'Nome Completo'} placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
{tipo !== 'empresa' && (
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 2, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={dataNascimento} onChangeText={(t) => setDataNascimento(aplicarMascaraData(t))} placeholder="Data de Nascimento (DD-MM-AAAA)" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<View style={[styles.input, { flex: 1, backgroundColor: cores.fundo, borderColor: cores.borda, justifyContent: 'center' }]}>
|
||||
<Text style={{ color: idade ? cores.texto : cores.placeholder, textAlign: 'center', fontWeight: '700' }}>
|
||||
{idade ? `${idade} anos` : 'Idade'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={telefone} onChangeText={setTelefone} keyboardType="phone-pad" placeholder="Nº Telemóvel" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={residencia} onChangeText={setResidencia} placeholder={tipo === 'empresa' ? 'Morada da Empresa' : 'Morada'} placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DADOS ESCOLARES (ALUNO) */}
|
||||
{tipo === 'aluno' && (
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="school" title="Dados Escolares" cores={cores} />
|
||||
<View style={styles.inputGroup}>
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={ano} onChangeText={setAno} keyboardType="numeric" placeholder="Ano (Ex: 12)" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 2, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nEscola} onChangeText={setNEscola} keyboardType="numeric" placeholder="Nº Aluno" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={curso} onChangeText={setCurso} placeholder="Turma/Curso (Ex: GPSI)" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* DADOS ESCOLARES (PROFESSOR) */}
|
||||
{tipo === 'professor' && (
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="library" title="Dados Profissionais" cores={cores} />
|
||||
<View style={styles.inputGroup}>
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nEscola}
|
||||
onChangeText={setNEscola}
|
||||
keyboardType="numeric"
|
||||
placeholder="Nº Prof."
|
||||
placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 2, backgroundColor: cores.inputFundo, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={curso}
|
||||
onChangeText={setCurso}
|
||||
placeholder="Curso Responsável"
|
||||
placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn, { backgroundColor: cores.azul }]}
|
||||
onPress={handleCriar}
|
||||
disabled={loading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Ionicons name="checkmark-circle" size={20} color="#FFF" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.submitBtnText}>GRAVAR NOVO REGISTO</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionHeader = ({ icon, title, cores }: any) => (
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={16} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.sectionTitleTxt, { color: cores.texto }]}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingTop: 10, paddingBottom: 60 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
backBtn: { width: 44, height: 44, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
subtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
|
||||
pillSelector: { flexDirection: 'row', borderRadius: 18, padding: 5, marginBottom: 25 },
|
||||
pillBtn: { flex: 1, height: 46, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
formContainer: { gap: 20 },
|
||||
sectionCard: { borderRadius: 28, padding: 22, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 },
|
||||
iconCircle: { width: 34, height: 34, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||
sectionTitleTxt: { fontSize: 15, fontWeight: '900', letterSpacing: -0.2 },
|
||||
|
||||
inputGroup: { gap: 12 },
|
||||
input: { height: 56, borderRadius: 16, borderWidth: 1.5, paddingHorizontal: 16, fontSize: 15, fontWeight: '700' },
|
||||
|
||||
submitBtn: { flexDirection: 'row', height: 60, borderRadius: 20, marginTop: 30, justifyContent: 'center', alignItems: 'center', elevation: 6, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.3, shadowRadius: 12 },
|
||||
submitBtnText: { color: '#fff', fontSize: 15, fontWeight: '900', letterSpacing: 1 },
|
||||
});
|
||||
|
||||
export default CriarAluno;
|
||||
@@ -1,10 +1,10 @@
|
||||
// app/Professor/Alunos/DetalhesAluno.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, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
Linking,
|
||||
Modal,
|
||||
ScrollView,
|
||||
@@ -19,7 +19,6 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
// --- UTILITÁRIOS DE DATA ---
|
||||
const formatarDataParaUI = (dataDB: string) => {
|
||||
if (!dataDB) return '';
|
||||
const parts = dataDB.split('-');
|
||||
@@ -57,7 +56,6 @@ const aplicarMascaraData = (text: string) => {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// --- TIPAGENS ---
|
||||
interface AlunoEditForm {
|
||||
nome: string;
|
||||
n_escola: string;
|
||||
@@ -80,22 +78,42 @@ const DetalhesAlunos = memo(() => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const [editForm, setEditForm] = useState<AlunoEditForm>({
|
||||
nome: '', n_escola: '', turma_curso: '', telefone: '', residencia: '', data_nascimento: '', email: ''
|
||||
});
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: '#2390a6',
|
||||
laranja: '#E38E00',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
inputFundo: isDarkMode ? '#252525' : '#EDF2F7'
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
vermelho: erroCor,
|
||||
verde: sucessoCor,
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const fetchAluno = async () => {
|
||||
if (!alunoId) return;
|
||||
try {
|
||||
@@ -143,7 +161,7 @@ const DetalhesAlunos = memo(() => {
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
Alert.alert("Erro", "Falha ao carregar dados.");
|
||||
showToast("Falha ao carregar os dados do aluno.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -156,13 +174,11 @@ const DetalhesAlunos = memo(() => {
|
||||
const dataFormatadaDB = formatarDataParaDB(editForm.data_nascimento);
|
||||
const novaIdade = calcularIdade(dataFormatadaDB || '');
|
||||
|
||||
// Não enviamos o 'n_escola' para garantir que os dados institucionais não são tocados
|
||||
const { data: d1, error: err1 } = await supabase.from('alunos').update({
|
||||
nome: editForm.nome,
|
||||
turma_curso: editForm.turma_curso
|
||||
}).eq('id', alunoId).select();
|
||||
|
||||
// Não enviamos o 'email' pelo mesmo motivo
|
||||
const { data: d2, error: err2 } = await supabase.from('profiles').update({
|
||||
telefone: editForm.telefone,
|
||||
residencia: editForm.residencia,
|
||||
@@ -174,14 +190,14 @@ const DetalhesAlunos = memo(() => {
|
||||
if (err2) throw err2;
|
||||
|
||||
if (!d1 || d1.length === 0 || !d2 || d2.length === 0) {
|
||||
throw new Error("Permissão Negada: O professor não tem direitos (RLS) para editar este aluno no Supabase.");
|
||||
throw new Error("Não tens permissões para editar este aluno.");
|
||||
}
|
||||
|
||||
Alert.alert("Sucesso", "Dados atualizados com sucesso!");
|
||||
showToast("Dados atualizados com sucesso!", "success");
|
||||
setModalVisible(false);
|
||||
fetchAluno();
|
||||
} catch (err: any) {
|
||||
Alert.alert("Erro na Gravação", err.message || "Não foi possível guardar. Verifica a ligação.");
|
||||
showToast(err.message || "Não foi possível guardar as alterações.", "error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -197,17 +213,35 @@ const DetalhesAlunos = memo(() => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
|
||||
<TouchableOpacity onPress={() => setModalVisible(true)} style={[styles.btnAction, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="create-outline" size={22} color={cores.laranja} />
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setModalVisible(true)} style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}>
|
||||
<Ionicons name="pencil" size={20} color={cores.laranja} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -219,31 +253,32 @@ const DetalhesAlunos = memo(() => {
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno?.nome}</Text>
|
||||
<Text style={[styles.alunoCurso, { color: cores.laranja }]}>{aluno?.turma_curso}</Text>
|
||||
<View style={[styles.roleBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="school" size={12} color={cores.azul} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.alunoCurso, { color: cores.azul }]}>{aluno?.turma_curso}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* INFORMAÇÕES PESSOAIS */}
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<DetailRow icon="school-outline" label="Nº Escola" value={aluno?.n_escola} cores={cores} />
|
||||
<DetailRow icon="id-card" label="Nº Escola" value={aluno?.n_escola} cores={cores} />
|
||||
<DetailRow
|
||||
icon="calendar-outline"
|
||||
icon="calendar"
|
||||
label="Nascimento / Idade"
|
||||
value={aluno?.perfil?.data_nascimento ? `${formatarDataParaUI(aluno.perfil.data_nascimento)} (${aluno.perfil.idade} anos)` : '-'}
|
||||
cores={cores}
|
||||
/>
|
||||
<DetailRow
|
||||
icon="mail-outline" label="Email" value={aluno?.perfil?.email} cores={cores}
|
||||
icon="mail" label="Email de Registo" value={aluno?.perfil?.email} cores={cores}
|
||||
onPress={aluno?.perfil?.email ? () => Linking.openURL(`mailto:${aluno.perfil.email}`) : null}
|
||||
/>
|
||||
<DetailRow
|
||||
icon="call-outline" label="Telemóvel" value={aluno?.perfil?.telefone} cores={cores}
|
||||
icon="call" label="Contacto Telefónico" value={aluno?.perfil?.telefone} cores={cores}
|
||||
onPress={aluno?.perfil?.telefone ? () => Linking.openURL(`tel:${aluno.perfil.telefone}`) : null}
|
||||
/>
|
||||
<DetailRow icon="location-outline" label="Residência" value={aluno?.perfil?.residencia} cores={cores} ultimo />
|
||||
<DetailRow icon="home" label="Residência" value={aluno?.perfil?.residencia} cores={cores} ultimo />
|
||||
</View>
|
||||
|
||||
{/* ESTÁGIO */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Plano de Estágio</Text>
|
||||
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
|
||||
@@ -258,7 +293,7 @@ const DetalhesAlunos = memo(() => {
|
||||
<Text style={styles.empresaNome}>{aluno?.estagio?.empresas?.nome || 'Empresa'}</Text>
|
||||
|
||||
<View style={styles.tutorInfo}>
|
||||
<Text style={styles.miniLabel}>TUTOR</Text>
|
||||
<Text style={styles.miniLabel}>TUTOR RESPONSÁVEL</Text>
|
||||
<Text style={styles.tutorNome}>{aluno?.estagio?.empresas?.tutor_nome || 'N/A'}</Text>
|
||||
<Text style={styles.tutorTel}>{aluno?.estagio?.empresas?.tutor_telefone || '-'}</Text>
|
||||
</View>
|
||||
@@ -272,41 +307,50 @@ const DetalhesAlunos = memo(() => {
|
||||
|
||||
<View style={styles.estagioFooter}>
|
||||
<View style={styles.periodoCol}>
|
||||
<Text style={styles.miniLabel}>INÍCIO</Text>
|
||||
<Text style={styles.miniLabel}>DATA DE INÍCIO</Text>
|
||||
<Text style={styles.footerVal}>{formatarDataParaUI(aluno?.estagio?.data_inicio) || '-'}</Text>
|
||||
</View>
|
||||
<View style={[styles.periodoCol, {alignItems: 'flex-end'}]}>
|
||||
<Text style={styles.miniLabel}>FIM</Text>
|
||||
<Text style={styles.miniLabel}>DATA DE FIM</Text>
|
||||
<Text style={styles.footerVal}>{formatarDataParaUI(aluno?.estagio?.data_fim) || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.noEstagio, { borderColor: cores.borda, backgroundColor: cores.card }]}>
|
||||
<Text style={[styles.noEstagioTxt, { color: cores.secundario }]}>Sem estágio atribuído</Text>
|
||||
<Ionicons name="alert-circle-outline" size={32} color={cores.secundario} style={{ marginBottom: 10 }} />
|
||||
<Text style={[styles.noEstagioTxt, { color: cores.secundario }]}>Nenhum estágio atribuído no momento</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* MODAL DE EDIÇÃO */}
|
||||
<Modal visible={modalVisible} animationType="slide" transparent>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
|
||||
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Editar Aluno</Text>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||
<Ionicons name="close" size={28} color={cores.texto} />
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Editar Aluno</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.laranja }]}>Atualizar Informações</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="close" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 40 }}>
|
||||
<EditInput label="Nome Completo" value={editForm.nome} onChange={(t: string) => setEditForm({...editForm, nome: t})} cores={cores} />
|
||||
|
||||
{/* CAMPOS INSTITUCIONAIS BLOQUEADOS (editable={false}) */}
|
||||
<EditInput label="Nº Escola (Institucional)" value={editForm.n_escola} cores={cores} editable={false} />
|
||||
<EditInput label="Email Institucional" value={editForm.email} cores={cores} editable={false} />
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<EditInput label="Nº Escola" value={editForm.n_escola} cores={cores} editable={false} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<EditInput label="Turma/Curso" value={editForm.turma_curso} onChange={(t: string) => setEditForm({...editForm, turma_curso: t})} cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<EditInput label="Turma/Curso" value={editForm.turma_curso} onChange={(t: string) => setEditForm({...editForm, turma_curso: t})} cores={cores} />
|
||||
<EditInput label="Email de Registo" value={editForm.email} cores={cores} editable={false} />
|
||||
<EditInput label="Telemóvel" value={editForm.telefone} onChange={(t: string) => setEditForm({...editForm, telefone: t})} cores={cores} keyboard="phone-pad" />
|
||||
<EditInput
|
||||
label="Nascimento (DD-MM-AAAA)"
|
||||
@@ -331,7 +375,6 @@ const DetalhesAlunos = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// --- COMPONENTES AUXILIARES ---
|
||||
interface EditInputProps {
|
||||
label: string;
|
||||
value: string;
|
||||
@@ -343,15 +386,17 @@ interface EditInputProps {
|
||||
}
|
||||
|
||||
const EditInput = ({ label, value, onChange, cores, keyboard = "default", maxLength, editable = true }: EditInputProps) => (
|
||||
<View style={{ marginBottom: 15 }}>
|
||||
<Text style={{ color: cores.secundario, fontSize: 10, fontWeight: '800', marginBottom: 5, textTransform: 'uppercase' }}>{label}</Text>
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: cores.inputFundo,
|
||||
backgroundColor: editable ? cores.inputFundo : cores.fundo,
|
||||
color: editable ? cores.texto : cores.secundario,
|
||||
opacity: editable ? 1 : 0.5 // Se não for editável, fica meio transparente
|
||||
borderColor: editable ? cores.azul : cores.borda,
|
||||
borderWidth: editable ? 1.5 : 1,
|
||||
opacity: editable ? 1 : 0.6
|
||||
}
|
||||
]}
|
||||
value={value}
|
||||
@@ -365,57 +410,76 @@ const EditInput = ({ label, value, onChange, cores, keyboard = "default", maxLen
|
||||
);
|
||||
|
||||
const DetailRow = ({ icon, label, value, cores, ultimo, onPress }: any) => (
|
||||
<TouchableOpacity disabled={!onPress} onPress={onPress} style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda + '50' }]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 12 }} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.rowLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<Text style={[styles.rowValue, { color: cores.texto }]}>{value || '-'}</Text>
|
||||
<TouchableOpacity disabled={!onPress} onPress={onPress} activeOpacity={0.7} style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda + '50' }]}>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={16} color={cores.azul} />
|
||||
</View>
|
||||
{onPress && <Ionicons name="chevron-forward" size={14} color={cores.secundario} />}
|
||||
<View style={{ flex: 1, paddingRight: 10 }}>
|
||||
<Text style={[styles.rowLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<Text style={[styles.rowValue, { color: cores.texto }]}>{value || 'Não definido'}</Text>
|
||||
</View>
|
||||
{onPress && <Ionicons name="chevron-forward" size={16} color={cores.secundario} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900' },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
profileSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 25, gap: 18 },
|
||||
avatar: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarTxt: { fontSize: 26, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
alunoCurso: { fontSize: 13, fontWeight: '800' },
|
||||
infoCard: { borderRadius: 25, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 5, marginBottom: 30 },
|
||||
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 },
|
||||
rowLabel: { fontSize: 9, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 },
|
||||
rowValue: { fontSize: 14, fontWeight: '700' },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginRight: 15 },
|
||||
sectionLine: { flex: 1, height: 1, opacity: 0.2 },
|
||||
estagioCard: { padding: 25, borderRadius: 30 },
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 15 },
|
||||
|
||||
profileSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 30, gap: 16 },
|
||||
avatar: { width: 70, height: 70, borderRadius: 24, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarTxt: { fontSize: 28, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5, marginBottom: 6 },
|
||||
roleBadge: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
alunoCurso: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
|
||||
infoCard: { borderRadius: 28, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 8, marginBottom: 35, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 },
|
||||
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14 },
|
||||
iconBox: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||
rowLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2, letterSpacing: 0.5 },
|
||||
rowValue: { fontSize: 15, fontWeight: '700' },
|
||||
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8, marginRight: 15 },
|
||||
sectionLine: { flex: 1, height: 1, opacity: 0.4 },
|
||||
|
||||
estagioCard: { padding: 24, borderRadius: 28, elevation: 4, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.3, shadowRadius: 12 },
|
||||
estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 },
|
||||
statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 },
|
||||
statusText: { color: '#fff', fontSize: 9, fontWeight: '900' },
|
||||
empresaNome: { color: '#fff', fontSize: 22, fontWeight: '900', marginBottom: 15 },
|
||||
tutorInfo: { marginBottom: 15 },
|
||||
miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 8, fontWeight: '900' },
|
||||
tutorNome: { color: '#fff', fontSize: 15, fontWeight: '800' },
|
||||
tutorTel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
|
||||
horarioBox: { backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15, marginBottom: 15 },
|
||||
horarioTxt: { color: '#fff', fontSize: 13, fontWeight: '700' },
|
||||
estagioFooter: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
statusText: { color: '#fff', fontSize: 10, fontWeight: '900', letterSpacing: 0.5 },
|
||||
empresaNome: { color: '#fff', fontSize: 22, fontWeight: '900', marginBottom: 18, letterSpacing: -0.5 },
|
||||
tutorInfo: { marginBottom: 18 },
|
||||
miniLabel: { color: 'rgba(255,255,255,0.6)', fontSize: 9, fontWeight: '900', letterSpacing: 0.5, marginBottom: 2 },
|
||||
tutorNome: { color: '#fff', fontSize: 16, fontWeight: '800' },
|
||||
tutorTel: { color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: '600' },
|
||||
horarioBox: { backgroundColor: 'rgba(255,255,255,0.1)', padding: 16, borderRadius: 18, marginBottom: 18 },
|
||||
horarioTxt: { color: '#fff', fontSize: 14, fontWeight: '700' },
|
||||
estagioFooter: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 },
|
||||
periodoCol: { flex: 1 },
|
||||
footerVal: { color: '#fff', fontSize: 13, fontWeight: '800' },
|
||||
noEstagio: { padding: 20, borderRadius: 20, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center' },
|
||||
noEstagioTxt: { fontWeight: '700', fontSize: 14 },
|
||||
footerVal: { color: '#fff', fontSize: 14, fontWeight: '800', marginTop: 2 },
|
||||
|
||||
noEstagio: { padding: 30, borderRadius: 24, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center', justifyContent: 'center' },
|
||||
noEstagioTxt: { fontWeight: '700', fontSize: 15 },
|
||||
|
||||
modalContainer: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
|
||||
modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 25, maxHeight: '90%' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 20, fontWeight: '900' },
|
||||
input: { borderRadius: 12, padding: 12, fontSize: 15, fontWeight: '600' },
|
||||
btnSave: { borderRadius: 15, padding: 16, alignItems: 'center', marginTop: 20, marginBottom: 40 },
|
||||
btnSaveTxt: { color: '#fff', fontWeight: '900', fontSize: 16 }
|
||||
modalContent: { borderTopLeftRadius: 36, borderTopRightRadius: 36, padding: 25, maxHeight: '92%' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 25 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
closeBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
input: { borderRadius: 16, paddingHorizontal: 16, height: 52, fontSize: 15, fontWeight: '700' },
|
||||
btnSave: { borderRadius: 18, height: 60, justifyContent: 'center', alignItems: 'center', marginTop: 20, elevation: 4, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
|
||||
btnSaveTxt: { color: '#fff', fontWeight: '900', fontSize: 16, letterSpacing: 0.5 }
|
||||
});
|
||||
|
||||
export default DetalhesAlunos;
|
||||
@@ -1,9 +1,10 @@
|
||||
// app/Professor/Estagios/index.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
Modal,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
@@ -18,7 +19,7 @@ import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
// --- Interfaces ---
|
||||
interface Aluno { id: string; nome: string; turma_curso: string; ano: number; }
|
||||
interface Aluno { id: string; nome: string; turma_curso: string; ano: number; n_escola?: string; }
|
||||
interface Empresa { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; }
|
||||
interface Estagio {
|
||||
id: string;
|
||||
@@ -49,20 +50,24 @@ export default function Estagios() {
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.12)' : '#FFF5F5',
|
||||
laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.12)' : '#FFF9F0',
|
||||
vermelho: '#EF4444',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6',
|
||||
verde: sucessoCor,
|
||||
vermelho: erroCor,
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
overlay: 'rgba(26, 54, 93, 0.8)',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Estados
|
||||
@@ -73,7 +78,11 @@ export default function Estagios() {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
|
||||
// ESTADO DO ALERTA CUSTOMIZADO (AGORA CENTRADO E BONITO)
|
||||
// TOAST ANIMADO
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
// ESTADO DO ALERTA CUSTOMIZADO
|
||||
const [customAlert, setCustomAlert] = useState({ visible: false, title: '', msg: '', tipo: 'warning' as 'warning' | 'error' });
|
||||
|
||||
const [estagioParaApagar, setEstagioParaApagar] = useState<{id: string, nome: string} | null>(null);
|
||||
@@ -88,12 +97,26 @@ export default function Estagios() {
|
||||
const [horasTotaisEstagio, setHorasTotaisEstagio] = useState('');
|
||||
const [searchMain, setSearchMain] = useState('');
|
||||
|
||||
// Estados de Pesquisa do Modal
|
||||
const [pesquisaAluno, setPesquisaAluno] = useState('');
|
||||
const [pesquisaEmpresa, setPesquisaEmpresa] = useState('');
|
||||
|
||||
// Horários Diários
|
||||
const [hManhaIni, setHManhaIni] = useState('');
|
||||
const [hManhaFim, setHManhaFim] = useState('');
|
||||
const [hTardeIni, setHTardeIni] = useState('');
|
||||
const [hTardeFim, setHTardeFim] = useState('');
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const showCustomAlert = (title: string, msg: string, tipo: 'warning' | 'error' = 'warning') => {
|
||||
setCustomAlert({ visible: true, title, msg, tipo });
|
||||
};
|
||||
@@ -105,13 +128,18 @@ export default function Estagios() {
|
||||
setLoading(true);
|
||||
const [resEstagios, resAlunos, resEmpresas] = await Promise.all([
|
||||
supabase.from('estagios').select('*, alunos(nome, turma_curso, ano), empresas(*)'),
|
||||
supabase.from('alunos').select('id, nome, turma_curso, ano').order('nome'),
|
||||
supabase.from('alunos').select('id, nome, turma_curso, ano, n_escola').order('nome'),
|
||||
supabase.from('empresas').select('*').order('nome')
|
||||
]);
|
||||
if (resEstagios.data) setEstagios(resEstagios.data);
|
||||
if (resAlunos.data) setAlunos(resAlunos.data);
|
||||
if (resEmpresas.data) setEmpresas(resEmpresas.data);
|
||||
} catch (e) { console.error(e); } finally { setLoading(false); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast("Erro ao carregar os dados", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const carregarHorariosEdicao = async (estagioId: string) => {
|
||||
@@ -130,6 +158,14 @@ export default function Estagios() {
|
||||
}
|
||||
};
|
||||
|
||||
// --- FUNÇÕES DE MÁSCARA ---
|
||||
const mascararData = (text: string) => {
|
||||
const num = text.replace(/\D/g, '');
|
||||
if (num.length > 6) return `${num.slice(0, 4)}-${num.slice(4, 6)}-${num.slice(6, 8)}`;
|
||||
if (num.length > 4) return `${num.slice(0, 4)}-${num.slice(4, 6)}`;
|
||||
return num;
|
||||
};
|
||||
|
||||
const aplicarMascaraHora = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '');
|
||||
if (cleaned.length >= 3) {
|
||||
@@ -138,6 +174,19 @@ export default function Estagios() {
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
// --- FUNÇÃO PARA HORÁRIOS RÁPIDOS ---
|
||||
const preencherHorario = (tipo: 'laboral' | 'manha' | 'tarde' | 'limpar') => {
|
||||
if (tipo === 'laboral') {
|
||||
setHManhaIni('09:00'); setHManhaFim('13:00'); setHTardeIni('14:00'); setHTardeFim('18:00');
|
||||
} else if (tipo === 'manha') {
|
||||
setHManhaIni('09:00'); setHManhaFim('13:00'); setHTardeIni(''); setHTardeFim('');
|
||||
} else if (tipo === 'tarde') {
|
||||
setHManhaIni(''); setHManhaFim(''); setHTardeIni('14:00'); setHTardeFim('18:00');
|
||||
} else {
|
||||
setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
|
||||
}
|
||||
};
|
||||
|
||||
const totalHorasDiarias = useMemo(() => {
|
||||
const calcularMinutos = (ini: string, fim: string) => {
|
||||
if (!ini || !fim || !ini.includes(':') || !fim.includes(':')) return 0;
|
||||
@@ -159,8 +208,9 @@ export default function Estagios() {
|
||||
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// --- LÓGICA MANTIDA INTACTA ---
|
||||
const calcularMagia = (tipo: 'dataFim' | 'horasTotais') => {
|
||||
if (!dataInicio) return showCustomAlert("Atenção", "Preenche a Data de Início primeiro!");
|
||||
if (!dataInicio || dataInicio.length < 10) return showCustomAlert("Atenção", "Preenche a Data de Início completa primeiro!");
|
||||
if (!hManhaIni && !hTardeIni) return showCustomAlert("Atenção", "Preenche primeiro o horário diário (Manhã e/ou Tarde)!");
|
||||
|
||||
const matchH = totalHorasDiarias.match(/(\d+)h/);
|
||||
@@ -192,7 +242,7 @@ export default function Estagios() {
|
||||
setDataFim(currentDate.toISOString().split('T')[0]);
|
||||
|
||||
} else if (tipo === 'horasTotais') {
|
||||
if (!dataFim) return showCustomAlert("Atenção", "Preenche a Data Fim (Prev.) primeiro!");
|
||||
if (!dataFim || dataFim.length < 10) return showCustomAlert("Atenção", "Preenche a Data Fim completa primeiro!");
|
||||
|
||||
let currentDate = new Date(dataInicio);
|
||||
const endDate = new Date(dataFim);
|
||||
@@ -283,6 +333,7 @@ export default function Estagios() {
|
||||
|
||||
handleFecharModal();
|
||||
fetchDados();
|
||||
showToast("Plano de estágio guardado com sucesso!", "success");
|
||||
} catch (error: any) {
|
||||
showCustomAlert("Erro ao Guardar", error.message, 'error');
|
||||
} finally {
|
||||
@@ -294,16 +345,24 @@ export default function Estagios() {
|
||||
if (!estagioParaApagar) return;
|
||||
setLoading(true);
|
||||
const { error } = await supabase.from('estagios').delete().eq('id', estagioParaApagar.id);
|
||||
if (!error) { setDeleteModalVisible(false); fetchDados(); }
|
||||
if (!error) {
|
||||
setDeleteModalVisible(false);
|
||||
fetchDados();
|
||||
showToast("Estágio apagado com sucesso.", "success");
|
||||
} else {
|
||||
showToast("Erro ao apagar o estágio.", "error");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleFecharModal = () => {
|
||||
setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
|
||||
setDataInicio(''); setDataFim(''); setHorasTotaisEstagio(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
|
||||
setPesquisaAluno(''); setPesquisaEmpresa('');
|
||||
setPasso(1);
|
||||
};
|
||||
|
||||
// --- FILTROS ---
|
||||
const estagiosFiltrados = useMemo(() => {
|
||||
const groups: Record<string, Estagio[]> = {};
|
||||
estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => {
|
||||
@@ -314,33 +373,63 @@ export default function Estagios() {
|
||||
return Object.keys(groups).map(titulo => ({ titulo, dados: groups[titulo] })).sort((a, b) => b.titulo.localeCompare(a.titulo));
|
||||
}, [estagios, searchMain]);
|
||||
|
||||
// Filtros em tempo real para os modais
|
||||
const alunosParaMostrar = useMemo(() => {
|
||||
return alunos.filter(a => a.nome.toLowerCase().includes(pesquisaAluno.toLowerCase()) || (a.n_escola && a.n_escola.includes(pesquisaAluno)));
|
||||
}, [alunos, pesquisaAluno]);
|
||||
|
||||
const empresasParaMostrar = useMemo(() => {
|
||||
return empresas.filter(e => e.nome.toLowerCase().includes(pesquisaEmpresa.toLowerCase()));
|
||||
}, [empresas, pesquisaEmpresa]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={() => router.back()}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul}/>
|
||||
</TouchableOpacity>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Plano de Estágios</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>{estagios.length} Alunos Colocados</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={fetchDados}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]} onPress={fetchDados}>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul}/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]} showsVerticalScrollIndicator={false}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<TextInput
|
||||
placeholder="Pesquisar por aluno ou turma..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
onChangeText={setSearchMain}
|
||||
/>
|
||||
{/* SEARCH BAR MODERNA */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
placeholder="Pesquisar por aluno ou turma..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
onChangeText={setSearchMain}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
@@ -375,10 +464,10 @@ export default function Estagios() {
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>{e.alunos?.nome.charAt(0)}</Text>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>{e.alunos?.nome.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{e.alunos?.nome}</Text>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{e.alunos?.nome}</Text>
|
||||
<View style={styles.empresaRow}>
|
||||
<Ionicons name="business-outline" size={13} color={cores.laranja} />
|
||||
<Text style={[styles.empresaText, { color: cores.secundario }]} numberOfLines={1}>{e.empresas?.nome}</Text>
|
||||
@@ -400,9 +489,10 @@ export default function Estagios() {
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.fab, { backgroundColor: cores.laranja, bottom: insets.bottom + 20 }]}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => { setPasso(1); setModalVisible(true); }}
|
||||
>
|
||||
<Ionicons name="add" size={28} color="#fff" />
|
||||
<Ionicons name="add" size={26} color="#fff" />
|
||||
<Text style={styles.fabText}>Novo Plano</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
@@ -425,57 +515,91 @@ export default function Estagios() {
|
||||
<ScrollView showsVerticalScrollIndicator={false} nestedScrollEnabled>
|
||||
{passo === 1 ? (
|
||||
<View style={{ gap: 20 }}>
|
||||
<Text style={styles.inputLabel}>Selecionar Estagiário</Text>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 220 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{Object.entries(alunos.reduce((acc, a) => {
|
||||
const key = `${a.ano}º ${a.turma_curso}`.trim().toUpperCase();
|
||||
if (!acc[key]) acc[key] = []; acc[key].push(a); return acc;
|
||||
}, {} as Record<string, Aluno[]>))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([turma, lista]) => (
|
||||
<View key={turma}>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{turma}</Text></View>
|
||||
{lista.map(a => (
|
||||
<TouchableOpacity
|
||||
key={a.id}
|
||||
style={[styles.pickerItem, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => setAlunoSelecionado(a)}
|
||||
>
|
||||
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{a.nome}</Text>
|
||||
{alunoSelecionado?.id === a.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* SELECIONAR ALUNO COM PESQUISA */}
|
||||
<View>
|
||||
<Text style={styles.inputLabel}>Selecionar Estagiário</Text>
|
||||
<View style={[styles.modalSearchBar, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={16} color={cores.secundario} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
placeholder="Procurar aluno..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
style={{ flex: 1, fontSize: 14, color: cores.texto }}
|
||||
value={pesquisaAluno}
|
||||
onChangeText={setPesquisaAluno}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 180 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{Object.entries(alunosParaMostrar.reduce((acc, a) => {
|
||||
const key = `${a.ano}º ${a.turma_curso}`.trim().toUpperCase();
|
||||
if (!acc[key]) acc[key] = []; acc[key].push(a); return acc;
|
||||
}, {} as Record<string, Aluno[]>))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([turma, lista]) => (
|
||||
<View key={turma}>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{turma}</Text></View>
|
||||
{lista.map(a => (
|
||||
<TouchableOpacity
|
||||
key={a.id}
|
||||
style={[styles.pickerItem, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => setAlunoSelecionado(a)}
|
||||
>
|
||||
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{a.nome}</Text>
|
||||
{alunoSelecionado?.id === a.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
{alunosParaMostrar.length === 0 && (
|
||||
<Text style={{ padding: 20, textAlign: 'center', color: cores.secundario }}>Sem resultados.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.inputLabel}>Entidade de Acolhimento</Text>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 220 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{Object.entries(empresas.reduce((acc, e) => {
|
||||
const key = (e.curso || 'GERAL').trim().toUpperCase();
|
||||
if (!acc[key]) acc[key] = []; acc[key].push(e); return acc;
|
||||
}, {} as Record<string, Empresa[]>))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([curso, lista]) => (
|
||||
<View key={curso}>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{curso}</Text></View>
|
||||
{lista.map(emp => (
|
||||
<TouchableOpacity
|
||||
key={emp.id}
|
||||
style={[styles.pickerItem, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => setEmpresaSelecionada(emp)}
|
||||
>
|
||||
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{emp.nome}</Text>
|
||||
{empresaSelecionada?.id === emp.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
{/* SELECIONAR EMPRESA COM PESQUISA */}
|
||||
<View>
|
||||
<Text style={styles.inputLabel}>Entidade de Acolhimento</Text>
|
||||
<View style={[styles.modalSearchBar, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={16} color={cores.secundario} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
placeholder="Procurar empresa..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
style={{ flex: 1, fontSize: 14, color: cores.texto }}
|
||||
value={pesquisaEmpresa}
|
||||
onChangeText={setPesquisaEmpresa}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 180 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{Object.entries(empresasParaMostrar.reduce((acc, e) => {
|
||||
const key = (e.curso || 'GERAL').trim().toUpperCase();
|
||||
if (!acc[key]) acc[key] = []; acc[key].push(e); return acc;
|
||||
}, {} as Record<string, Empresa[]>))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([curso, lista]) => (
|
||||
<View key={curso}>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{curso}</Text></View>
|
||||
{lista.map(emp => (
|
||||
<TouchableOpacity
|
||||
key={emp.id}
|
||||
style={[styles.pickerItem, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => setEmpresaSelecionada(emp)}
|
||||
>
|
||||
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{emp.nome}</Text>
|
||||
{empresaSelecionada?.id === emp.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
{empresasParaMostrar.length === 0 && (
|
||||
<Text style={{ padding: 20, textAlign: 'center', color: cores.secundario }}>Sem resultados.</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ gap: 18 }}>
|
||||
@@ -485,7 +609,15 @@ export default function Estagios() {
|
||||
<View style={styles.rowInputs}>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={styles.miniLabel}>DATA INÍCIO</Text>
|
||||
<TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataInicio} onChangeText={setDataInicio} placeholder="AAAA-MM-DD"/>
|
||||
<TextInput
|
||||
style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
value={dataInicio}
|
||||
onChangeText={(t) => setDataInicio(mascararData(t))}
|
||||
placeholder="AAAA-MM-DD"
|
||||
maxLength={10}
|
||||
keyboardType="numeric"
|
||||
placeholderTextColor={cores.secundario}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flex:1}}>
|
||||
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6}}>
|
||||
@@ -494,7 +626,15 @@ export default function Estagios() {
|
||||
<Ionicons name="color-wand" size={16} color={cores.laranja} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataFim} onChangeText={setDataFim} placeholder="AAAA-MM-DD"/>
|
||||
<TextInput
|
||||
style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
value={dataFim}
|
||||
onChangeText={(t) => setDataFim(mascararData(t))}
|
||||
placeholder="AAAA-MM-DD"
|
||||
maxLength={10}
|
||||
keyboardType="numeric"
|
||||
placeholderTextColor={cores.secundario}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -507,10 +647,11 @@ export default function Estagios() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]}
|
||||
style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
value={horasTotaisEstagio}
|
||||
onChangeText={setHorasTotaisEstagio}
|
||||
keyboardType="numeric"
|
||||
placeholderTextColor={cores.secundario}
|
||||
placeholder="Ex: 400"
|
||||
/>
|
||||
</View>
|
||||
@@ -521,41 +662,59 @@ export default function Estagios() {
|
||||
<Text style={[styles.groupTitle, { color: cores.azul }]}>Horário de Trabalho</Text>
|
||||
<View style={[styles.totalBadge, { backgroundColor: cores.laranja }]}><Text style={styles.totalText}>{totalHorasDiarias}/dia</Text></View>
|
||||
</View>
|
||||
|
||||
{/* BOTÕES DE PREENCHIMENTO RÁPIDO */}
|
||||
<View style={styles.quickTimeRow}>
|
||||
<TouchableOpacity style={[styles.quickTimeBtn, { backgroundColor: cores.azulSuave }]} onPress={() => preencherHorario('laboral')}>
|
||||
<Text style={[styles.quickTimeBtnText, { color: cores.azul }]}>Laboral (9-18h)</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.quickTimeBtn, { backgroundColor: cores.azulSuave }]} onPress={() => preencherHorario('manha')}>
|
||||
<Text style={[styles.quickTimeBtnText, { color: cores.azul }]}>Manhã (9-13h)</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.quickTimeBtn, { backgroundColor: cores.vermelhoSuave }]} onPress={() => preencherHorario('limpar')}>
|
||||
<Ionicons name="trash-outline" size={14} color={cores.vermelho} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.tabelaRow}>
|
||||
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Manhã</Text>
|
||||
<TextInput
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda}]}
|
||||
value={hManhaIni}
|
||||
onChangeText={(txt) => setHManhaIni(aplicarMascaraHora(txt))}
|
||||
keyboardType="numeric"
|
||||
maxLength={5}
|
||||
placeholderTextColor={cores.secundario}
|
||||
placeholder="09:00"
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda}]}
|
||||
value={hManhaFim}
|
||||
onChangeText={(txt) => setHManhaFim(aplicarMascaraHora(txt))}
|
||||
keyboardType="numeric"
|
||||
maxLength={5}
|
||||
placeholderTextColor={cores.secundario}
|
||||
placeholder="13:00"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.tabelaRow}>
|
||||
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Tarde</Text>
|
||||
<TextInput
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda}]}
|
||||
value={hTardeIni}
|
||||
onChangeText={(txt) => setHTardeIni(aplicarMascaraHora(txt))}
|
||||
keyboardType="numeric"
|
||||
maxLength={5}
|
||||
placeholderTextColor={cores.secundario}
|
||||
placeholder="14:00"
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
|
||||
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda}]}
|
||||
value={hTardeFim}
|
||||
onChangeText={(txt) => setHTardeFim(aplicarMascaraHora(txt))}
|
||||
keyboardType="numeric"
|
||||
maxLength={5}
|
||||
placeholderTextColor={cores.secundario}
|
||||
placeholder="18:00"
|
||||
/>
|
||||
</View>
|
||||
@@ -563,8 +722,8 @@ export default function Estagios() {
|
||||
|
||||
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Text style={[styles.groupTitle, { color: cores.azul }]}>Responsável na Empresa</Text>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, backgroundColor: cores.card}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholder="Nome do Tutor"/>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, backgroundColor: cores.card, marginTop: 10}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Telemóvel"/>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholderTextColor={cores.secundario} placeholder="Nome do Tutor"/>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, backgroundColor: cores.card, borderColor: cores.borda, marginTop: 10}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholderTextColor={cores.secundario} placeholder="Telemóvel"/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
@@ -580,12 +739,12 @@ export default function Estagios() {
|
||||
onPress={() => {
|
||||
if(passo === 1) {
|
||||
if(alunoSelecionado && empresaSelecionada) setPasso(2);
|
||||
else showCustomAlert("Atenção", "Selecione o aluno e a empresa antes de avançar.");
|
||||
else showToast("Selecione o aluno e a empresa antes de avançar.", "info");
|
||||
} else salvarEstagio();
|
||||
}}
|
||||
style={[styles.btnModalPri, { backgroundColor: cores.azul }]}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={{color:'#fff', fontWeight: '900'}}>{passo === 1 ? "CONTINUAR" : "FINALIZAR"}</Text>}
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={{color:'#fff', fontWeight: '900', letterSpacing: 0.5}}>{passo === 1 ? "CONTINUAR" : "FINALIZAR"}</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -602,7 +761,6 @@ export default function Estagios() {
|
||||
<View style={styles.alertOverlay}>
|
||||
<View style={[styles.alertCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
|
||||
{/* Botão de fechar (X vermelho pequeno) centrado no topo */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => setCustomAlert({ ...customAlert, visible: false })}
|
||||
@@ -619,7 +777,6 @@ export default function Estagios() {
|
||||
{customAlert.title.replace(' ⚖️', '')}
|
||||
</Text>
|
||||
|
||||
{/* Mostra a balança apenas se for o erro do Limite Legal */}
|
||||
{customAlert.title.includes('Excedido') && (
|
||||
<Text style={styles.alertEmoji}>⚖️</Text>
|
||||
)}
|
||||
@@ -632,7 +789,7 @@ export default function Estagios() {
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* DELETE MODAL ORIGINAL */}
|
||||
{/* DELETE MODAL (COM NOVO DESIGN ARREDONDADO) */}
|
||||
<Modal visible={deleteModalVisible} transparent animationType="fade">
|
||||
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
|
||||
<View style={[styles.deleteCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
@@ -644,8 +801,8 @@ export default function Estagios() {
|
||||
Irá remover o vínculo de <Text style={{fontWeight: '900', color: cores.texto}}>{estagioParaApagar?.nome}</Text>. Esta ação é irreversível.
|
||||
</Text>
|
||||
<View style={styles.deleteFooter}>
|
||||
<TouchableOpacity style={[styles.deleteBtnCancel, { backgroundColor: cores.fundo }]} onPress={() => setDeleteModalVisible(false)}>
|
||||
<Text style={[styles.deleteBtnText, { color: cores.texto }]}>FECHAR</Text>
|
||||
<TouchableOpacity style={[styles.deleteBtnCancel, { borderColor: cores.borda, borderWidth: 1 }]} onPress={() => setDeleteModalVisible(false)}>
|
||||
<Text style={[styles.deleteBtnText, { color: cores.secundario }]}>CANCELAR</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.deleteBtnConfirm, { backgroundColor: cores.vermelho }]} onPress={confirmarEliminacao}>
|
||||
<Text style={[styles.deleteBtnText, { color: '#fff' }]}>ELIMINAR</Text>
|
||||
@@ -660,103 +817,88 @@ export default function Estagios() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5, marginBottom: 25 },
|
||||
searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
searchSection: { paddingHorizontal: 4, marginBottom: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '600' },
|
||||
|
||||
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 },
|
||||
estagioCard: { padding: 18, borderRadius: 28, borderWidth: 1, marginBottom: 14, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
estagioCard: { padding: 16, borderRadius: 24, borderWidth: 1, marginBottom: 14, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 14 },
|
||||
avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 20, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
empresaRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 },
|
||||
empresaText: { fontSize: 13, fontWeight: '600' },
|
||||
timeBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12, borderWidth: 1 },
|
||||
timeText: { fontSize: 11, fontWeight: '900' },
|
||||
fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
timeBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10, borderWidth: 1 },
|
||||
timeText: { fontSize: 10, fontWeight: '900' },
|
||||
|
||||
fab: { position: 'absolute', right: 20, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 6, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
|
||||
modalOverlay: { flex: 1, justifyContent: 'flex-end', alignItems: 'center' },
|
||||
modalContent: { borderTopLeftRadius: 45, borderTopRightRadius: 45, padding: 28, height: '92%', width: '100%' },
|
||||
modalContent: { borderTopLeftRadius: 36, borderTopRightRadius: 36, padding: 25, height: '92%', width: '100%' },
|
||||
modalIndicator: { width: 45, height: 5, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, alignSelf: 'center', marginBottom: 20 },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 4, letterSpacing: 0.5 },
|
||||
closeBtn: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 10, marginLeft: 5, letterSpacing: 0.5 },
|
||||
|
||||
inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 10, marginLeft: 5, letterSpacing: 0.5 },
|
||||
modalSearchBar: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 16, paddingHorizontal: 14, height: 46, marginBottom: 12 },
|
||||
pickerContainer: { borderWidth: 1.5, borderRadius: 24, overflow: 'hidden', width: '100%' },
|
||||
groupHeader: { paddingVertical: 10, paddingHorizontal: 18 },
|
||||
groupHeaderText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
|
||||
pickerItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 18, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
|
||||
pickerItemText: { fontSize: 15, fontWeight: '700' },
|
||||
modernGroup: { padding: 20, borderRadius: 30, borderWidth: 1, marginBottom: 18 },
|
||||
|
||||
modernGroup: { padding: 20, borderRadius: 28, borderWidth: 1, marginBottom: 18 },
|
||||
groupTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 0.5 },
|
||||
rowInputs: { flexDirection: 'row', gap: 15 },
|
||||
miniLabel: { fontSize: 9, fontWeight: '900', color: '#94A3B8', marginBottom: 6, marginLeft: 2 },
|
||||
modernInput: { paddingVertical: 14, paddingHorizontal: 16, borderRadius: 16, fontSize: 14, fontWeight: '700', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
|
||||
modernInput: { paddingVertical: 14, paddingHorizontal: 16, borderRadius: 16, fontSize: 14, fontWeight: '700', borderWidth: 1 },
|
||||
|
||||
quickTimeRow: { flexDirection: 'row', gap: 8, marginBottom: 15 },
|
||||
quickTimeBtn: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 },
|
||||
quickTimeBtnText: { fontSize: 11, fontWeight: '800' },
|
||||
tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 },
|
||||
tabelaLabel: { flex: 1, fontSize: 14, fontWeight: '800' },
|
||||
tabelaInput: { flex: 1, padding: 14, borderRadius: 16, textAlign: 'center', fontSize: 14, fontWeight: '800', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
|
||||
tabelaInput: { flex: 1, padding: 14, borderRadius: 16, textAlign: 'center', fontSize: 14, fontWeight: '800', borderWidth: 1 },
|
||||
|
||||
totalBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
totalText: { color: '#fff', fontSize: 11, fontWeight: '900' },
|
||||
|
||||
modalFooter: { flexDirection: 'row', gap: 15, marginTop: 15 },
|
||||
btnModalPri: { flex: 2, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center' },
|
||||
btnModalSec: { flex: 1, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5 },
|
||||
deleteCard: { width: '85%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center', marginBottom: 30 },
|
||||
btnModalPri: { flex: 2, height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
btnModalSec: { flex: 1, height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5 },
|
||||
|
||||
deleteCard: { width: '85%', borderRadius: 36, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center', marginBottom: 30, elevation: 10 },
|
||||
deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12 },
|
||||
deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12, letterSpacing: -0.5 },
|
||||
deleteSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 },
|
||||
deleteFooter: { flexDirection: 'row', gap: 15, width: '100%' },
|
||||
deleteBtnCancel: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnCancel: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnText: { fontSize: 14, fontWeight: '900' },
|
||||
|
||||
// --- ESTILOS ESPECÍFICOS PARA O AVISO CENTRADO ---
|
||||
alertOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(26, 54, 93, 0.8)',
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
alertCard: {
|
||||
width: '85%',
|
||||
borderRadius: 30,
|
||||
padding: 30,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
elevation: 10,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
alertIconBg: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 15
|
||||
},
|
||||
alertTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '900',
|
||||
textAlign: 'center',
|
||||
marginBottom: 5
|
||||
},
|
||||
alertEmoji: {
|
||||
fontSize: 28,
|
||||
marginBottom: 10
|
||||
},
|
||||
alertSubtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
fontWeight: '500'
|
||||
},
|
||||
alertOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.6)', paddingHorizontal: 20 },
|
||||
alertCard: { width: '85%', borderRadius: 36, padding: 30, alignItems: 'center', borderWidth: 1, elevation: 10, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 10 },
|
||||
alertIconBg: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
alertTitle: { fontSize: 20, fontWeight: '900', textAlign: 'center', marginBottom: 8, letterSpacing: -0.5 },
|
||||
alertEmoji: { fontSize: 28, marginBottom: 10 },
|
||||
alertSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '600' },
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
// app/Professor/Alunos/Faltas.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
FlatList,
|
||||
Linking,
|
||||
Modal,
|
||||
@@ -53,23 +53,40 @@ const ListaFaltasProfessor = memo(() => {
|
||||
const [faltas, setFaltas] = useState<Falta[]>([]);
|
||||
const [loadingFaltas, setLoadingFaltas] = useState(false);
|
||||
|
||||
// Estados do Toast
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Ajustado para design premium
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.12)' : '#FEF2F2',
|
||||
verde: '#10B981',
|
||||
vermelho: erroCor,
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
verde: sucessoCor,
|
||||
verdeSuave: isDarkMode ? 'rgba(16, 185, 129, 0.15)' : '#DCFCE7',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -99,6 +116,7 @@ const ListaFaltasProfessor = memo(() => {
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Erro ao carregar as turmas.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -123,7 +141,7 @@ const ListaFaltasProfessor = memo(() => {
|
||||
|
||||
if (errEstagio) throw errEstagio;
|
||||
|
||||
// Se não houver estágio, não há intervalo para filtrar, logo não mostramos faltas "soltas"
|
||||
// Se não houver estágio, não há intervalo para filtrar
|
||||
if (!estagioData) {
|
||||
setFaltas([]);
|
||||
return;
|
||||
@@ -143,7 +161,7 @@ const ListaFaltasProfessor = memo(() => {
|
||||
setFaltas(data || []);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Alert.alert("Erro", "Falha ao carregar o histórico de faltas do estágio.");
|
||||
showToast("Falha ao carregar as faltas.", "error");
|
||||
} finally {
|
||||
setLoadingFaltas(false);
|
||||
}
|
||||
@@ -160,7 +178,7 @@ const ListaFaltasProfessor = memo(() => {
|
||||
try {
|
||||
await Linking.openURL(url);
|
||||
} catch (error) {
|
||||
Alert.alert("Erro", "Não foi possível abrir o ficheiro.");
|
||||
showToast("Não foi possível abrir o ficheiro.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,14 +196,30 @@ const ListaFaltasProfessor = memo(() => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER */}
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
@@ -193,24 +227,24 @@ const ListaFaltasProfessor = memo(() => {
|
||||
|
||||
<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>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={fetchAlunos}
|
||||
>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* SEARCH */}
|
||||
{/* SEARCH SECTION MODERNA */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Pesquisar aluno ou nº..."
|
||||
placeholder="Pesquisar por nome ou nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -225,9 +259,11 @@ const ListaFaltasProfessor = memo(() => {
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ marginBottom: 25 }}>
|
||||
<View style={{ marginBottom: 30 }}>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
@@ -237,7 +273,7 @@ const ListaFaltasProfessor = memo(() => {
|
||||
{item.alunos.map((aluno) => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => abrirFaltas(aluno)}
|
||||
>
|
||||
@@ -248,16 +284,15 @@ const ListaFaltasProfessor = memo(() => {
|
||||
</View>
|
||||
|
||||
<View style={styles.alunoInfo}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{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>
|
||||
<Ionicons name="warning-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.idText, { color: cores.secundario }]}>Nº {aluno.n_escola} • Gerir Faltas</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>
|
||||
<Ionicons name="eye-outline" size={16} color={cores.vermelho} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -265,8 +300,10 @@ const ListaFaltasProfessor = memo(() => {
|
||||
)}
|
||||
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 style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="people-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhum aluno encontrado.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
@@ -276,10 +313,11 @@ const ListaFaltasProfessor = memo(() => {
|
||||
<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>
|
||||
|
||||
<View style={[styles.modalHeader, { borderBottomColor: cores.borda }]}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Registo de Faltas</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.laranja }]}>{alunoSelecionado?.nome}</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.laranja }]} numberOfLines={1}>{alunoSelecionado?.nome}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="close" size={24} color={cores.azul} />
|
||||
@@ -287,13 +325,16 @@ const ListaFaltasProfessor = memo(() => {
|
||||
</View>
|
||||
|
||||
{loadingFaltas ? (
|
||||
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
|
||||
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} size="large" />
|
||||
) : (
|
||||
<ScrollView contentContainerStyle={{ padding: 24 }} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView contentContainerStyle={{ padding: 20, paddingBottom: insets.bottom + 60 }} 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 style={[styles.emptyContainer, { marginTop: 60 }]}>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: cores.verdeSuave }]}>
|
||||
<Ionicons name="happy-outline" size={40} color={cores.verde} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhuma falta registada!</Text>
|
||||
<Text style={[styles.emptySubText, { color: cores.secundario, opacity: 0.7 }]}>Este aluno tem a assiduidade em dia.</Text>
|
||||
</View>
|
||||
) : (
|
||||
faltas.map((f) => (
|
||||
@@ -305,12 +346,19 @@ const ListaFaltasProfessor = memo(() => {
|
||||
<Text style={[styles.faltaData, { color: cores.texto }]}>
|
||||
{new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</Text>
|
||||
<Text style={[styles.statusFaltaText, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
|
||||
{f.justificacao_url ? 'JUSTIFICADA' : 'INJUSTIFICADA'}
|
||||
</Text>
|
||||
<View style={[styles.tagJustificada, { backgroundColor: f.justificacao_url ? cores.verdeSuave : cores.vermelhoSuave }]}>
|
||||
<Ionicons name={f.justificacao_url ? "checkmark-circle" : "close-circle"} size={12} color={f.justificacao_url ? cores.verde : cores.vermelho} />
|
||||
<Text style={[styles.statusFaltaText, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
|
||||
{f.justificacao_url ? 'JUSTIFICADA' : 'INJUSTIFICADA'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{f.justificacao_url && (
|
||||
<TouchableOpacity style={[styles.btnView, { backgroundColor: cores.azul }]} onPress={() => verDocumento(f.justificacao_url!)}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnView, { backgroundColor: cores.azul }]}
|
||||
onPress={() => verDocumento(f.justificacao_url!)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="eye-outline" size={18} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
@@ -330,39 +378,52 @@ const ListaFaltasProfessor = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
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 },
|
||||
|
||||
searchSection: { paddingHorizontal: 20, marginBottom: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
|
||||
listPadding: { paddingHorizontal: 20, 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 },
|
||||
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' },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 20, fontWeight: '900' },
|
||||
alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 },
|
||||
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5 },
|
||||
idText: { fontSize: 12, fontWeight: '700' },
|
||||
statusBadge: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', 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: { 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' }
|
||||
modalContent: { height: '88%', borderTopLeftRadius: 32, borderTopRightRadius: 32, overflow: 'hidden' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, paddingBottom: 15, borderBottomWidth: 1 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 4 },
|
||||
closeBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
faltaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 14, borderWidth: 1, elevation: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 5 },
|
||||
dateIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
faltaData: { fontSize: 15, fontWeight: '800', marginBottom: 6 },
|
||||
tagJustificada: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, gap: 4 },
|
||||
statusFaltaText: { fontSize: 10, fontWeight: '900', letterSpacing: 0.5 },
|
||||
btnView: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 6, shadowOffset: { width: 0, height: 3 } },
|
||||
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
emptyText: { fontSize: 15, fontWeight: '700' },
|
||||
emptySubText: { fontSize: 13, fontWeight: '600', marginTop: 4 }
|
||||
});
|
||||
|
||||
export default ListaFaltasProfessor;
|
||||
@@ -1,10 +1,10 @@
|
||||
// app/Professor/Alunos/ListaAlunosProfessor.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
FlatList,
|
||||
Modal,
|
||||
RefreshControl,
|
||||
@@ -47,25 +47,45 @@ const ListaAlunosProfessor = memo(() => {
|
||||
const [alunoParaEliminar, setAlunoParaEliminar] = useState<Aluno | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// Estados para o Toast
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: '#2390a6',
|
||||
laranja: '#E38E00',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
vermelho: '#EF4444',
|
||||
vermelho: erroCor,
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
verde: sucessoCor,
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('alunos')
|
||||
.select(`id, nome, n_escola, ano, turma_curso`)
|
||||
.select('id, nome, n_escola, ano, turma_curso')
|
||||
.order('ano', { ascending: false })
|
||||
.order('nome', { ascending: true });
|
||||
|
||||
@@ -89,6 +109,7 @@ const ListaAlunosProfessor = memo(() => {
|
||||
})));
|
||||
} catch (err) {
|
||||
console.error("Erro ao carregar lista:", err);
|
||||
showToast("Falha ao carregar a lista de alunos.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -101,7 +122,6 @@ const ListaAlunosProfessor = memo(() => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
|
||||
// APAGAR NO PROFILES (O Cascade do SQL trata das tabelas 'alunos' e 'estagios')
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.delete()
|
||||
@@ -110,12 +130,10 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Se o data vier vazio, o RLS bloqueou o delete no profiles
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error("O servidor recusou apagar o perfil. Verifica se as políticas RLS na tabela 'profiles' permitem DELETE.");
|
||||
throw new Error("O servidor recusou a eliminação. Verifique permissões RLS.");
|
||||
}
|
||||
|
||||
// Sucesso no banco -> Atualizar UI local
|
||||
setTurmas(prev => prev.map(turma => ({
|
||||
...turma,
|
||||
alunos: turma.alunos.filter(a => a.id !== alunoParaEliminar.id)
|
||||
@@ -123,11 +141,11 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
setShowDeleteModal(false);
|
||||
setAlunoParaEliminar(null);
|
||||
Alert.alert("Sucesso", "Aluno e todos os dados vinculados foram eliminados.");
|
||||
showToast("Aluno apagado com sucesso do sistema.", "success");
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("ERRO AO APAGAR:", err);
|
||||
Alert.alert("Erro Crítico", err.message);
|
||||
showToast(err.message || "Não foi possível apagar o aluno.", "error");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
@@ -153,19 +171,36 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER */}
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={() => router.back()}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Alunos</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Gestão de Turmas</Text>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Lista de alunos</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={fetchAlunos}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]} onPress={fetchAlunos}>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -173,10 +208,10 @@ const ListaAlunosProfessor = memo(() => {
|
||||
{/* PESQUISA */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Procurar aluno..."
|
||||
placeholder="Procurar por nome ou nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -190,10 +225,11 @@ const ListaAlunosProfessor = memo(() => {
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 100 }]}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 40 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ marginBottom: 25 }}>
|
||||
<View style={{ marginBottom: 30 }}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
@@ -202,7 +238,7 @@ const ListaAlunosProfessor = memo(() => {
|
||||
{item.alunos.map((aluno) => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push({ pathname: '/Professor/Alunos/DetalhesAluno', params: { alunoId: aluno.id } })}
|
||||
onLongPress={() => {
|
||||
@@ -214,34 +250,55 @@ const ListaAlunosProfessor = memo(() => {
|
||||
<Text style={[styles.avatarText, { 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.idText, { color: cores.secundario }]}>Nº {aluno.n_escola}</Text>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{aluno.nome}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>
|
||||
<Ionicons name="id-card-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.idText, { color: cores.secundario }]}>Nº {aluno.n_escola}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="trash-outline" size={18} color={cores.vermelho} style={{ opacity: 0.5 }} />
|
||||
<TouchableOpacity
|
||||
style={styles.trashBtn}
|
||||
onPress={() => {
|
||||
setAlunoParaEliminar(aluno);
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color={cores.vermelho} style={{ opacity: 0.7 }} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="people-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhum aluno encontrado.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MODAL DE ELIMINAÇÃO */}
|
||||
<Modal visible={showDeleteModal} transparent animationType="fade">
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalCard, { backgroundColor: cores.fundo }]}>
|
||||
<View style={[styles.modalCard, { backgroundColor: cores.card }]}>
|
||||
<View style={[styles.warningIconBox, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="trash-bin-outline" size={32} color={cores.vermelho} />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Permanentemente?</Text>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Ação Irreversível</Text>
|
||||
<Text style={[styles.modalDesc, { color: cores.secundario }]}>
|
||||
Estás prestes a apagar o perfil de <Text style={{fontWeight:'900', color: cores.texto}}>{alunoParaEliminar?.nome}</Text>.
|
||||
Isto removerá o acesso à app, dados escolares e estágios. **Vai dar merda** se apagares por engano!
|
||||
Tens a certeza que queres eliminar o registo de <Text style={{fontWeight:'900', color: cores.texto}}>{alunoParaEliminar?.nome}</Text>?
|
||||
</Text>
|
||||
<Text style={[styles.modalWarning, { color: cores.vermelho }]}>
|
||||
Isto apagará também os estágios e presenças!
|
||||
</Text>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnModal, { borderColor: cores.borda, borderWidth: 1 }]}
|
||||
style={[styles.btnModal, { borderColor: cores.borda, borderWidth: 1.5 }]}
|
||||
onPress={() => setShowDeleteModal(false)}
|
||||
>
|
||||
<Text style={[styles.btnText, { color: cores.secundario }]}>Cancelar</Text>
|
||||
@@ -255,7 +312,7 @@ const ListaAlunosProfessor = memo(() => {
|
||||
{isDeleting ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Text style={[styles.btnText, { color: '#fff' }]}>Eliminar Tudo</Text>
|
||||
<Text style={[styles.btnText, { color: '#fff' }]}>Sim, Apagar</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -263,14 +320,6 @@ const ListaAlunosProfessor = memo(() => {
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* BOTÃO FLUTUANTE */}
|
||||
<TouchableOpacity
|
||||
style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]}
|
||||
onPress={() => router.push('/Professor/Alunos/CriarAluno')}
|
||||
>
|
||||
<Ionicons name="person-add" size={24} color="#fff" />
|
||||
<Text style={styles.fabText}>Novo Aluno</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
@@ -278,34 +327,47 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 22, fontWeight: '900' },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
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 },
|
||||
|
||||
searchSection: { paddingHorizontal: 20, marginBottom: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
|
||||
listPadding: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase' },
|
||||
sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 },
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1 },
|
||||
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' },
|
||||
idText: { fontSize: 13, fontWeight: '600' },
|
||||
fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5 },
|
||||
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10 },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', alignItems: 'center', padding: 30 },
|
||||
modalCard: { width: '100%', borderRadius: 32, padding: 24, alignItems: 'center' },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 20, fontWeight: '900' },
|
||||
alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 },
|
||||
idText: { fontSize: 13, fontWeight: '700' },
|
||||
trashBtn: { padding: 8 },
|
||||
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
emptyText: { fontSize: 15, fontWeight: '700' },
|
||||
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', padding: 24 },
|
||||
modalCard: { width: '100%', borderRadius: 32, padding: 24, alignItems: 'center', elevation: 10 },
|
||||
warningIconBox: { width: 70, height: 70, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 20, fontWeight: '900', marginBottom: 10, textAlign: 'center' },
|
||||
modalDesc: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 25 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', marginBottom: 8, textAlign: 'center', letterSpacing: -0.5 },
|
||||
modalDesc: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 10 },
|
||||
modalWarning: { fontSize: 12, fontWeight: '800', textAlign: 'center', textTransform: 'uppercase', marginBottom: 25 },
|
||||
modalButtons: { flexDirection: 'row', gap: 12, width: '100%' },
|
||||
btnModal: { flex: 1, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
btnText: { fontSize: 16, fontWeight: '800' }
|
||||
btnModal: { flex: 1, height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
btnText: { fontSize: 15, fontWeight: '800', letterSpacing: 0.5 }
|
||||
});
|
||||
|
||||
export default ListaAlunosProfessor;
|
||||
@@ -39,14 +39,15 @@ const Presencas = memo(() => {
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Atualizado para o design premium
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
@@ -105,14 +106,13 @@ const Presencas = memo(() => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER EPVC */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
@@ -120,24 +120,23 @@ const Presencas = memo(() => {
|
||||
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Presenças</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Controlo Diário</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={fetchAlunos}
|
||||
>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 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} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Procurar aluno..."
|
||||
placeholder="Procurar por nome ou nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -151,11 +150,12 @@ const Presencas = memo(() => {
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 40 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ marginBottom: 25 }}>
|
||||
{/* SEPARADOR DE TURMA */}
|
||||
<View style={{ marginBottom: 30 }}>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
@@ -165,7 +165,7 @@ const Presencas = memo(() => {
|
||||
{item.alunos.map((aluno) => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
@@ -181,15 +181,15 @@ const Presencas = memo(() => {
|
||||
</View>
|
||||
|
||||
<View style={styles.alunoInfo}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{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>
|
||||
<Ionicons name="calendar-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.idText, { color: cores.secundario }]}>Nº {aluno.n_escola} • Abrir Registo</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statusBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="chevron-forward" size={16} color={cores.azul} />
|
||||
<View style={[styles.statusBadge, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -197,8 +197,10 @@ const Presencas = memo(() => {
|
||||
)}
|
||||
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 style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="people-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhum aluno encontrado.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
@@ -210,27 +212,33 @@ const Presencas = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
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 },
|
||||
|
||||
searchSection: { paddingHorizontal: 20, marginBottom: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
|
||||
listPadding: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
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 },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
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' },
|
||||
alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 },
|
||||
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5 },
|
||||
idText: { fontSize: 12, fontWeight: '700' },
|
||||
statusBadge: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
emptyText: { fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
|
||||
export default Presencas;
|
||||
export default Presencas;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// app/(Professor)/SumariosAlunos.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
FlatList,
|
||||
Modal,
|
||||
RefreshControl,
|
||||
@@ -44,26 +45,43 @@ const SumariosAlunos = memo(() => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// Estados do Modal
|
||||
// Estados do Modal e Avisos
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [alunoSelecionado, setAlunoSelecionado] = useState<Aluno | null>(null);
|
||||
const [sumarios, setSumarios] = useState<Sumario[]>([]);
|
||||
const [loadingSumarios, setLoadingSumarios] = useState(false);
|
||||
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
vermelho: erroCor,
|
||||
verde: sucessoCor,
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -93,6 +111,7 @@ const SumariosAlunos = memo(() => {
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Erro ao carregar as turmas.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -131,8 +150,10 @@ const SumariosAlunos = memo(() => {
|
||||
|
||||
if (error) throw error;
|
||||
setSumarios(data || []);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast("Erro ao carregar o caderno.", "error");
|
||||
} finally {
|
||||
setLoadingSumarios(false);
|
||||
}
|
||||
@@ -140,7 +161,6 @@ const SumariosAlunos = memo(() => {
|
||||
|
||||
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: '' });
|
||||
@@ -165,13 +185,30 @@ const SumariosAlunos = memo(() => {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER */}
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
@@ -179,24 +216,24 @@ const SumariosAlunos = memo(() => {
|
||||
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Sumários</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Caderno de Registos</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={fetchAlunos}
|
||||
>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* SEARCH */}
|
||||
{/* SEARCH SECTION MODERNA */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Pesquisar aluno..."
|
||||
placeholder="Pesquisar por nome ou nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -211,9 +248,11 @@ const SumariosAlunos = memo(() => {
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ marginBottom: 25 }}>
|
||||
<View style={{ marginBottom: 30 }}>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
@@ -223,7 +262,7 @@ const SumariosAlunos = memo(() => {
|
||||
{item.alunos.map((aluno) => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => abrirSumarios(aluno)}
|
||||
>
|
||||
@@ -234,20 +273,28 @@ const SumariosAlunos = memo(() => {
|
||||
</View>
|
||||
|
||||
<View style={styles.alunoInfo}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{aluno.nome}</Text>
|
||||
<View style={styles.idRow}>
|
||||
<Ionicons name="book-outline" size={13} color={cores.secundario} />
|
||||
<Ionicons name="book-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.idText, { color: cores.secundario }]}>Nº {aluno.n_escola} • Ver Caderno</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statusBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
|
||||
<View style={[styles.statusBadge, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="people-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhum aluno encontrado.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -255,10 +302,11 @@ const SumariosAlunos = memo(() => {
|
||||
<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 style={[styles.modalHeader, { borderBottomColor: cores.borda }]}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Caderno Diário</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.laranja }]} numberOfLines={1}>{alunoSelecionado?.nome}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="close" size={24} color={cores.azul} />
|
||||
@@ -266,20 +314,23 @@ const SumariosAlunos = memo(() => {
|
||||
</View>
|
||||
|
||||
{loadingSumarios ? (
|
||||
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
|
||||
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} size="large" />
|
||||
) : (
|
||||
<ScrollView contentContainerStyle={{ padding: 24, paddingBottom: 60 }} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView contentContainerStyle={{ padding: 20, paddingBottom: insets.bottom + 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 style={[styles.emptyContainer, { marginTop: 60 }]}>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="document-text-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Sem sumários registados.</Text>
|
||||
<Text style={[styles.emptySubText, { color: cores.secundario, opacity: 0.7 }]}>Os diários de bordo irão aparecer aqui.</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} />
|
||||
<Ionicons name="calendar-outline" size={14} color={cores.azul} />
|
||||
<Text style={[styles.dateTagText, { color: cores.azul }]}>
|
||||
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
|
||||
</Text>
|
||||
@@ -303,39 +354,52 @@ const SumariosAlunos = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
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 },
|
||||
|
||||
searchSection: { paddingHorizontal: 20, marginBottom: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
|
||||
listPadding: { paddingHorizontal: 20, 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 },
|
||||
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' },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
avatar: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 20, fontWeight: '900' },
|
||||
alunoInfo: { flex: 1, marginLeft: 15, paddingRight: 10 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 },
|
||||
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5 },
|
||||
idText: { fontSize: 12, fontWeight: '700' },
|
||||
statusBadge: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
|
||||
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' },
|
||||
modalContent: { height: '88%', borderTopLeftRadius: 32, borderTopRightRadius: 32, overflow: 'hidden' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20, paddingBottom: 15, borderBottomWidth: 1 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 4 },
|
||||
closeBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
sumarioCard: { padding: 20, borderRadius: 24, marginBottom: 16, borderWidth: 1, elevation: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 5 },
|
||||
sumarioTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 16 },
|
||||
dateTag: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12 },
|
||||
dateTagText: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase' },
|
||||
dotLine: { flex: 1, height: 1.5, marginLeft: 12, opacity: 0.4 },
|
||||
sumarioTexto: { fontSize: 15, lineHeight: 24, fontWeight: '600' },
|
||||
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
emptyText: { fontSize: 15, fontWeight: '700' },
|
||||
emptySubText: { fontSize: 13, fontWeight: '600', marginTop: 4 }
|
||||
});
|
||||
|
||||
export default SumariosAlunos;
|
||||
@@ -1,9 +1,10 @@
|
||||
// app/Professor/Empresas/DetalhesEmpresa.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, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
@@ -28,6 +29,16 @@ export interface Empresa {
|
||||
curso: string;
|
||||
}
|
||||
|
||||
// Interface para os detalhes do estágio no Modal
|
||||
interface DetalhesEstagio {
|
||||
id: string;
|
||||
data_inicio: string;
|
||||
data_fim: string;
|
||||
horas_diarias: string;
|
||||
horas_totais: number;
|
||||
horarios: { periodo: string; hora_inicio: string; hora_fim: string }[];
|
||||
}
|
||||
|
||||
const DetalhesEmpresa = memo(() => {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
@@ -36,6 +47,8 @@ const DetalhesEmpresa = memo(() => {
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const empresaOriginal: Empresa = useMemo(() => {
|
||||
if (!params.empresa) return {} as Empresa;
|
||||
@@ -52,23 +65,45 @@ const DetalhesEmpresa = memo(() => {
|
||||
const [loadingAlunos, setLoadingAlunos] = useState(true);
|
||||
const [editando, setEditando] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
// Estados para o Modal de Detalhes do Estágio
|
||||
const [estagioModalVisible, setEstagioModalVisible] = useState(false);
|
||||
const [alunoSelecionadoParaEstagio, setAlunoSelecionadoParaEstagio] = useState<{ id: string; nome: string } | null>(null);
|
||||
const [detalhesEstagio, setDetalhesEstagio] = useState<DetalhesEstagio | null>(null);
|
||||
const [loadingEstagio, setLoadingEstagio] = useState(false);
|
||||
|
||||
// ESTADOS DO TOAST ANIMADO
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: '#FFF5F5',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
overlay: 'rgba(26, 54, 93, 0.8)',
|
||||
vermelho: erroCor,
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
verde: sucessoCor,
|
||||
verdeSuave: isDarkMode ? 'rgba(16, 185, 129, 0.15)' : '#DCFCE7',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
useEffect(() => {
|
||||
if (empresaLocal.id) carregarAlunos();
|
||||
}, [empresaLocal.id]);
|
||||
@@ -89,6 +124,46 @@ const DetalhesEmpresa = memo(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Função para carregar os detalhes do estágio quando clica num aluno
|
||||
const abrirDetalhesEstagio = async (aluno: { id: string; nome: string }) => {
|
||||
setAlunoSelecionadoParaEstagio(aluno);
|
||||
setEstagioModalVisible(true);
|
||||
setLoadingEstagio(true);
|
||||
setDetalhesEstagio(null);
|
||||
|
||||
try {
|
||||
// Vai buscar o estágio ativo deste aluno nesta empresa
|
||||
const { data: estagioData, error: estagioError } = await supabase
|
||||
.from('estagios')
|
||||
.select('id, data_inicio, data_fim, horas_diarias, horas_totais')
|
||||
.eq('aluno_id', aluno.id)
|
||||
.eq('empresa_id', empresaLocal.id)
|
||||
.order('data_inicio', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (estagioError) throw estagioError;
|
||||
|
||||
// Vai buscar os horários
|
||||
const { data: horariosData } = await supabase
|
||||
.from('horarios_estagio')
|
||||
.select('periodo, hora_inicio, hora_fim')
|
||||
.eq('estagio_id', estagioData.id);
|
||||
|
||||
setDetalhesEstagio({
|
||||
...estagioData,
|
||||
horarios: horariosData || []
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Não foi possível carregar os detalhes do estágio.', 'error');
|
||||
setEstagioModalVisible(false);
|
||||
} finally {
|
||||
setLoadingEstagio(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -103,10 +178,9 @@ const DetalhesEmpresa = memo(() => {
|
||||
if (error) throw error;
|
||||
|
||||
setEditando(false);
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => setShowSuccess(false), 3000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Dados atualizados com sucesso!', 'success');
|
||||
} catch (error: any) {
|
||||
showToast(error.message || 'Não foi possível guardar as alterações.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -121,69 +195,201 @@ const DetalhesEmpresa = memo(() => {
|
||||
router.back();
|
||||
} catch (e) {
|
||||
setShowDeleteModal(false);
|
||||
alert('Não é possível apagar empresas com estágios ativos.');
|
||||
showToast('Não é possível apagar empresas com estágios ativos.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper para formatar a data
|
||||
const formatarData = (dataStr: string) => {
|
||||
if (!dataStr) return 'Não definida';
|
||||
const partes = dataStr.split('-');
|
||||
if (partes.length === 3) return `${partes[2]}-${partes[1]}-${partes[0]}`;
|
||||
return dataStr;
|
||||
};
|
||||
|
||||
// Helper para formatar a hora
|
||||
const formatarHora = (horaStr: string) => {
|
||||
if (!horaStr) return '';
|
||||
return horaStr.slice(0, 5); // "09:00:00" -> "09:00"
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{showSuccess && (
|
||||
<View style={[styles.toast, { backgroundColor: cores.azul, top: insets.top + 10 }]}>
|
||||
<Ionicons name="checkmark-circle" size={20} color="#fff" />
|
||||
<Text style={styles.toastText}>Dados atualizados!</Text>
|
||||
</View>
|
||||
)}
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
{/* DELETE MODAL */}
|
||||
<Modal visible={showDeleteModal} transparent animationType="fade">
|
||||
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<View style={[styles.deleteCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={[styles.deleteIconBg, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="warning" size={35} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Entidade?</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>
|
||||
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Eliminar Entidade?</Text>
|
||||
<Text style={[styles.deleteSubtitle, { color: cores.secundario }]}>
|
||||
Esta ação é irreversível. A empresa <Text style={{fontWeight: '900', color: cores.texto}}>{empresaLocal.nome}</Text> será removida do sistema.
|
||||
</Text>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<View style={styles.deleteFooter}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtn, { backgroundColor: isDarkMode ? '#2D2D2D' : '#E2E8F0' }]}
|
||||
style={[styles.deleteBtnCancel, { borderColor: cores.borda, borderWidth: 1 }]}
|
||||
onPress={() => setShowDeleteModal(false)}
|
||||
>
|
||||
<Text style={[styles.modalBtnTxt, { color: cores.texto }]}>Cancelar</Text>
|
||||
<Text style={[styles.deleteBtnText, { color: cores.secundario }]}>CANCELAR</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtn, { backgroundColor: cores.vermelho }]}
|
||||
style={[styles.deleteBtnConfirm, { backgroundColor: cores.vermelho }]}
|
||||
onPress={confirmDelete}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.modalBtnTxt, { color: '#fff' }]}>Confirmar</Text>}
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.deleteBtnText, { color: '#fff' }]}>ELIMINAR</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
{/* MODAL DE DETALHES DO ESTÁGIO (NOVO) */}
|
||||
<Modal visible={estagioModalVisible} transparent animationType="slide" onRequestClose={() => setEstagioModalVisible(false)}>
|
||||
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay, justifyContent: 'flex-end', paddingHorizontal: 0, paddingBottom: 0 }]}>
|
||||
<View style={[styles.estagioModalContent, { backgroundColor: cores.card, paddingBottom: insets.bottom + 20 }]}>
|
||||
<View style={styles.modalIndicator} />
|
||||
|
||||
<View style={styles.modalHeader}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Plano de Estágio</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.laranja }]} numberOfLines={1}>{alunoSelecionadoParaEstagio?.nome}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setEstagioModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.fundo }]}>
|
||||
<Ionicons name="close" size={22} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{loadingEstagio ? (
|
||||
<ActivityIndicator size="large" color={cores.azul} style={{ marginVertical: 40 }} />
|
||||
) : detalhesEstagio ? (
|
||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 20 }}>
|
||||
|
||||
{/* DATAS E HORAS */}
|
||||
<View style={[styles.infoBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<View style={styles.infoRow}>
|
||||
<View style={styles.infoCol}>
|
||||
<Text style={[styles.infoLabel, { color: cores.secundario }]}>INÍCIO</Text>
|
||||
<Text style={[styles.infoValue, { color: cores.texto }]}>{formatarData(detalhesEstagio.data_inicio)}</Text>
|
||||
</View>
|
||||
<View style={[styles.infoCol, { alignItems: 'flex-end' }]}>
|
||||
<Text style={[styles.infoLabel, { color: cores.secundario }]}>FIM (PREV.)</Text>
|
||||
<Text style={[styles.infoValue, { color: cores.texto }]}>{formatarData(detalhesEstagio.data_fim)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<View style={styles.infoCol}>
|
||||
<Text style={[styles.infoLabel, { color: cores.secundario }]}>HORAS/DIA</Text>
|
||||
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.badgeTxt, { color: cores.azul }]}>{detalhesEstagio.horas_diarias || 'N/A'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.infoCol, { alignItems: 'flex-end' }]}>
|
||||
<Text style={[styles.infoLabel, { color: cores.secundario }]}>HORAS TOTAIS</Text>
|
||||
<View style={[styles.badge, { backgroundColor: cores.laranja }]}>
|
||||
<Text style={[styles.badgeTxt, { color: '#fff' }]}>{detalhesEstagio.horas_totais || '0'}h</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* HORÁRIOS */}
|
||||
<View style={[styles.infoBox, { backgroundColor: cores.fundo, borderColor: cores.borda, marginTop: 15 }]}>
|
||||
<Text style={[styles.infoTitle, { color: cores.texto }]}>Horário de Trabalho</Text>
|
||||
|
||||
{detalhesEstagio.horarios.length > 0 ? (
|
||||
detalhesEstagio.horarios.map((horario, index) => (
|
||||
<View key={index} style={styles.horarioRow}>
|
||||
<Ionicons name={horario.periodo === 'Manhã' ? "partly-sunny-outline" : "sunny-outline"} size={18} color={cores.secundario} />
|
||||
<Text style={[styles.horarioPeriodo, { color: cores.secundario }]}>{horario.periodo}</Text>
|
||||
<View style={styles.horarioTags}>
|
||||
<Text style={[styles.horarioTime, { color: cores.texto, backgroundColor: cores.card }]}>
|
||||
{formatarHora(horario.hora_inicio)}
|
||||
</Text>
|
||||
<Text style={{ color: cores.secundario, marginHorizontal: 4 }}>-</Text>
|
||||
<Text style={[styles.horarioTime, { color: cores.texto, backgroundColor: cores.card }]}>
|
||||
{formatarHora(horario.hora_fim)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text style={[styles.empty, { color: cores.secundario, marginTop: 5 }]}>Horários não definidos.</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* BOTÃO PARA PRESENÇAS */}
|
||||
<TouchableOpacity
|
||||
style={[styles.btnActionSecondary, { borderColor: cores.azul, backgroundColor: cores.azulSuave, marginTop: 20 }]}
|
||||
onPress={() => {
|
||||
setEstagioModalVisible(false);
|
||||
router.push({
|
||||
pathname: '/Professor/HistoricoPresencas',
|
||||
params: { alunoId: alunoSelecionadoParaEstagio?.id, nome: alunoSelecionadoParaEstagio?.nome }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Ionicons name="calendar-outline" size={20} color={cores.azul} />
|
||||
<Text style={[styles.btnActionSecondaryTxt, { color: cores.azul }]}>Ver Histórico de Presenças</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
</ScrollView>
|
||||
) : (
|
||||
<Text style={[styles.empty, { color: cores.secundario, marginTop: 20 }]}>Não foi possível carregar os dados.</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<View style={{alignItems: 'center', flex: 1, paddingHorizontal: 10}}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha Técnica</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>ID: #{empresaLocal.id?.slice(0, 5)}</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>ID: #{empresaLocal.id?.toString().slice(0, 5)}</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: editando ? cores.laranja : 'transparent', borderColor: editando ? cores.laranja : cores.borda }]}
|
||||
style={[
|
||||
styles.btnAction,
|
||||
{
|
||||
backgroundColor: editando ? cores.laranja : cores.card,
|
||||
borderColor: editando ? cores.laranja : cores.borda
|
||||
}
|
||||
]}
|
||||
onPress={() => {
|
||||
if(editando) setEmpresaLocal({...empresaOriginal});
|
||||
setEditando(!editando);
|
||||
@@ -222,17 +428,25 @@ const DetalhesEmpresa = memo(() => {
|
||||
<Text style={styles.countBadgeTxt}>{loadingAlunos ? '-' : alunos.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{loadingAlunos ? <ActivityIndicator size="small" color={cores.azul} /> : alunos.length > 0 ? (
|
||||
{loadingAlunos ? <ActivityIndicator size="small" color={cores.azul} style={{ marginVertical: 10 }} /> : alunos.length > 0 ? (
|
||||
alunos.map((aluno, i) => (
|
||||
<View key={aluno.id} style={[styles.alunoRow, i !== alunos.length - 1 && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.alunoRow, i !== alunos.length - 1 && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
|
||||
onPress={() => abrirDetalhesEstagio(aluno)}
|
||||
>
|
||||
<View style={[styles.miniAvatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="person" size={14} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.alunoName, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.secundario} opacity={0.3} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.alunoName, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={{ fontSize: 11, color: cores.secundario, marginTop: 2, fontWeight: '600' }}>Ver plano de estágio</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.5} />
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : <Text style={[styles.empty, { color: cores.secundario }]}>Nenhum aluno vinculado a esta entidade.</Text>}
|
||||
) : <Text style={[styles.empty, { color: cores.secundario }]}>Nenhum aluno vinculado a esta entidade no momento.</Text>}
|
||||
</View>
|
||||
|
||||
<View style={styles.footerActions}>
|
||||
@@ -261,12 +475,19 @@ const DetalhesEmpresa = memo(() => {
|
||||
const ModernField = ({ label, value, editable, cores, icon, ...props }: any) => (
|
||||
<View style={styles.fieldContainer}>
|
||||
<View style={styles.labelRow}>
|
||||
<Ionicons name={icon} size={12} color={cores.secundario} />
|
||||
<Ionicons name={icon} size={14} color={cores.secundario} />
|
||||
<Text style={[styles.fieldLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
</View>
|
||||
{editable ? (
|
||||
<TextInput
|
||||
style={[styles.input, { color: cores.texto, backgroundColor: cores.fundo, borderColor: cores.azul }]}
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
color: cores.texto,
|
||||
backgroundColor: cores.inputFundo,
|
||||
borderColor: cores.azul
|
||||
}
|
||||
]}
|
||||
value={value}
|
||||
selectionColor={cores.laranja}
|
||||
{...props}
|
||||
@@ -281,43 +502,81 @@ const ModernField = ({ label, value, editable, cores, icon, ...props }: any) =>
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
toast: { position: 'absolute', left: 25, right: 25, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, gap: 12, elevation: 8 },
|
||||
toastText: { color: '#fff', fontSize: 14, fontWeight: '900' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
card: { padding: 22, borderRadius: 32, borderWidth: 1, marginBottom: 18, elevation: 2, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, gap: 10 },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
|
||||
card: { padding: 24, borderRadius: 28, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 22, gap: 10 },
|
||||
sideLine: { width: 4, height: 18, borderRadius: 2 },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
|
||||
fieldContainer: { marginBottom: 18 },
|
||||
labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 6, marginLeft: 4 },
|
||||
fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
input: { fontSize: 15, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 16, borderWidth: 1.5 },
|
||||
labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 8, marginLeft: 4 },
|
||||
fieldLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
input: { fontSize: 15, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 14, borderRadius: 16, borderWidth: 1.5 },
|
||||
readOnly: { paddingHorizontal: 16, paddingVertical: 14, borderRadius: 16, borderWidth: 1 },
|
||||
readOnlyTxt: { fontSize: 15, fontWeight: '700' },
|
||||
countBadge: { width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' },
|
||||
readOnlyTxt: { fontSize: 15, fontWeight: '600' },
|
||||
|
||||
countBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginLeft: 'auto' },
|
||||
countBadgeTxt: { color: '#fff', fontSize: 11, fontWeight: '900' },
|
||||
|
||||
alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14 },
|
||||
miniAvatar: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||
alunoName: { fontSize: 15, fontWeight: '700', flex: 1 },
|
||||
empty: { textAlign: 'center', fontSize: 13, fontWeight: '600', paddingVertical: 10, fontStyle: 'italic' },
|
||||
footerActions: { marginTop: 10 },
|
||||
btnSave: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', elevation: 5, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
miniAvatar: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginRight: 14 },
|
||||
alunoName: { fontSize: 15, fontWeight: '800', letterSpacing: -0.3 },
|
||||
empty: { textAlign: 'center', fontSize: 14, fontWeight: '600', paddingVertical: 15, opacity: 0.7 },
|
||||
|
||||
footerActions: { marginTop: 10, marginBottom: 20 },
|
||||
btnSave: { height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 8 },
|
||||
btnRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
btnSaveTxt: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
btnDel: { height: 56, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 1.5, borderStyle: 'dashed' },
|
||||
btnSaveTxt: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
btnDel: { height: 56, borderRadius: 18, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 1.5, borderStyle: 'dashed' },
|
||||
btnDelTxt: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 25 },
|
||||
modalContent: { width: '100%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1 },
|
||||
iconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '900', marginBottom: 12 },
|
||||
modalSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 },
|
||||
modalButtons: { flexDirection: 'row', gap: 15 },
|
||||
modalBtn: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
modalBtnTxt: { fontSize: 15, fontWeight: '900' }
|
||||
|
||||
// DELETE MODAL STYLES
|
||||
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.6)', paddingHorizontal: 20 },
|
||||
deleteCard: { width: '85%', borderRadius: 36, padding: 30, alignItems: 'center', borderWidth: 1, elevation: 10, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 10 },
|
||||
deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
deleteTitle: { fontSize: 22, fontWeight: '900', textAlign: 'center', marginBottom: 12, letterSpacing: -0.5 },
|
||||
deleteSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, fontWeight: '500', marginBottom: 30 },
|
||||
deleteFooter: { flexDirection: 'row', gap: 15, width: '100%' },
|
||||
deleteBtnCancel: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnText: { fontSize: 14, fontWeight: '900' },
|
||||
|
||||
// ESTAGIO MODAL STYLES (NOVO)
|
||||
estagioModalContent: { width: '100%', borderTopLeftRadius: 36, borderTopRightRadius: 36, padding: 25, maxHeight: '85%' },
|
||||
modalIndicator: { width: 45, height: 5, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, alignSelf: 'center', marginBottom: 20 },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSubtitle: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', marginTop: 4, letterSpacing: 0.5 },
|
||||
closeBtn: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
infoBox: { borderRadius: 24, borderWidth: 1, padding: 20 },
|
||||
infoTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 0.5 },
|
||||
infoRow: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
infoCol: { flex: 1 },
|
||||
infoLabel: { fontSize: 9, fontWeight: '900', textTransform: 'uppercase', marginBottom: 6, letterSpacing: 0.5 },
|
||||
infoValue: { fontSize: 15, fontWeight: '800' },
|
||||
divider: { height: 1, opacity: 0.5, marginVertical: 15 },
|
||||
|
||||
badge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start' },
|
||||
badgeTxt: { fontSize: 13, fontWeight: '900' },
|
||||
|
||||
horarioRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
||||
horarioPeriodo: { fontSize: 13, fontWeight: '800', width: 60, marginLeft: 8 },
|
||||
horarioTags: { flexDirection: 'row', alignItems: 'center', flex: 1, justifyContent: 'flex-end' },
|
||||
horarioTime: { fontSize: 14, fontWeight: '700', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, overflow: 'hidden' },
|
||||
|
||||
btnActionSecondary: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: 56, borderRadius: 18, borderWidth: 1.5, borderStyle: 'dashed', gap: 8 },
|
||||
btnActionSecondaryTxt: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }
|
||||
});
|
||||
|
||||
export default DetalhesEmpresa;
|
||||
@@ -1,15 +1,11 @@
|
||||
// app/Professor/Empresas/ListaEmpresasProfessor.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Animated,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
SectionList,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
@@ -40,25 +36,42 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
const [empresas, setEmpresas] = useState<Empresa[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const [form, setForm] = useState({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
|
||||
// ESTADOS DO TOAST
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
// Cores EPVC
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
vermelho: erroCor,
|
||||
verde: sucessoCor,
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const fetchEmpresas = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -70,7 +83,7 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
if (error) throw error;
|
||||
setEmpresas(data || []);
|
||||
} catch (error: any) {
|
||||
Alert.alert('Erro', 'Não foi possível carregar as empresas.');
|
||||
showToast('Não foi possível carregar as empresas.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -103,47 +116,32 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
})).sort((a, b) => a.title.localeCompare(b.title));
|
||||
}, [search, empresas]);
|
||||
|
||||
const criarEmpresa = async () => {
|
||||
if (!form.nome || !form.morada || !form.tutorNome || !form.tutorTelefone || !form.curso) {
|
||||
Alert.alert('Atenção', 'Preencha todos os campos.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('empresas')
|
||||
.insert([{
|
||||
nome: form.nome.trim(),
|
||||
morada: form.morada.trim(),
|
||||
tutor_nome: form.tutorNome.trim(),
|
||||
tutor_telefone: form.tutorTelefone.trim(),
|
||||
curso: form.curso.trim().toUpperCase(),
|
||||
}])
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
setEmpresas(prev => [...prev, data![0]]);
|
||||
setModalVisible(false);
|
||||
setForm({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
|
||||
Alert.alert('Sucesso', 'Empresa registada com sucesso!');
|
||||
} catch (error: any) {
|
||||
Alert.alert('Erro', error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER EPVC */}
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.backBtn, { borderColor: cores.borda }]}
|
||||
style={[styles.backBtn, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
@@ -155,19 +153,12 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
{empresas.length} parcerias ativas
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.addBtn, { backgroundColor: cores.laranja }]}
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Ionicons name="add" size={30} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* PESQUISA */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Procurar empresa ou curso..."
|
||||
@@ -187,6 +178,7 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
sections={secoesAgrupadas}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
stickySectionHeadersEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderSectionHeader={({ section: { title } }) => (
|
||||
@@ -198,119 +190,76 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
)}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.empresaCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', params: { empresa: JSON.stringify(item) } })}
|
||||
>
|
||||
<View style={[styles.empresaIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="business-outline" size={24} color={cores.azul} />
|
||||
<Ionicons name="business-outline" size={22} color={cores.azul} />
|
||||
</View>
|
||||
|
||||
<View style={styles.empresaInfo}>
|
||||
<Text style={[styles.empresaNome, { color: cores.texto }]}>{item.nome}</Text>
|
||||
<Text style={[styles.empresaNome, { color: cores.texto }]} numberOfLines={1}>{item.nome}</Text>
|
||||
<View style={styles.tutorRow}>
|
||||
<Ionicons name="location-outline" size={12} color={cores.secundario} />
|
||||
<Ionicons name="location-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.tutorText, { color: cores.secundario }]} numberOfLines={1}>{item.morada}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.arrowCircle, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
|
||||
<View style={[styles.arrowCircle, { borderColor: cores.borda, backgroundColor: cores.fundo }]}>
|
||||
<Ionicons name="chevron-forward" size={16} color={cores.azul} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="search-outline" size={60} color={cores.borda} />
|
||||
<Text style={{ color: cores.secundario, marginTop: 15, fontWeight: '700' }}>Nenhuma entidade encontrada.</Text>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="business-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhuma entidade encontrada.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
|
||||
{/* MODAL NOVO REGISTO */}
|
||||
<Modal visible={modalVisible} animationType="slide" transparent>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
|
||||
<View style={styles.modalHeader}>
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Registar Parceiro</Text>
|
||||
<Text style={[styles.modalSub, { color: cores.laranja }]}>Nova entidade de estágio</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={styles.closeBtn}>
|
||||
<Ionicons name="close" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 30 }}>
|
||||
<ModernInput label="Nome da Empresa" icon="business" value={form.nome} onChangeText={(v:any)=>setForm({...form, nome:v})} cores={cores} />
|
||||
<ModernInput label="Curso Associado" icon="school" value={form.curso} onChangeText={(v:any)=>setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI, MULT, etc." />
|
||||
<ModernInput label="Localização / Morada" icon="map" value={form.morada} onChangeText={(v:any)=>setForm({...form, morada:v})} cores={cores} />
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
|
||||
|
||||
<ModernInput label="Tutor na Empresa" icon="person" value={form.tutorNome} onChangeText={(v:any)=>setForm({...form, tutorNome:v})} cores={cores} />
|
||||
<ModernInput label="Contacto do Tutor" icon="call" value={form.tutorTelefone} onChangeText={(v:any)=>setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} />
|
||||
|
||||
<TouchableOpacity style={[styles.saveBtn, { backgroundColor: cores.azul }]} onPress={criarEmpresa}>
|
||||
<Text style={styles.saveBtnText}>Confirmar Registo</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const ModernInput = ({ label, icon, cores, ...props }: any) => (
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<View style={[styles.inputContainer, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 12 }} />
|
||||
<TextInput {...props} style={[styles.textInput, { color: cores.texto }]} placeholderTextColor={cores.secundario} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 1000, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
headerTitle: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8 },
|
||||
headerSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
addBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 8 },
|
||||
searchSection: { paddingHorizontal: 24, marginVertical: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' },
|
||||
loadingCenter: { marginTop: 60, alignItems: 'center' },
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
listPadding: { paddingHorizontal: 24 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 30, marginBottom: 15 },
|
||||
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
|
||||
searchSection: { paddingHorizontal: 20, marginBottom: 10 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
|
||||
loadingCenter: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
listPadding: { paddingHorizontal: 20 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 20, marginBottom: 15 },
|
||||
sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 },
|
||||
empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 28, marginBottom: 14, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 12 },
|
||||
empresaIcon: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
empresaInfo: { flex: 1, marginLeft: 15 },
|
||||
empresaNome: { fontSize: 17, fontWeight: '800', letterSpacing: -0.3 },
|
||||
tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 4 },
|
||||
tutorText: { fontSize: 12, fontWeight: '600' },
|
||||
arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(26, 54, 93, 0.8)', justifyContent: 'flex-end' },
|
||||
modalContent: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 28, maxHeight: '90%' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 30 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 },
|
||||
closeBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' },
|
||||
divider: { height: 1, marginVertical: 20, opacity: 0.5 },
|
||||
inputWrapper: { marginBottom: 20 },
|
||||
inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 58, borderRadius: 18, borderWidth: 1.5 },
|
||||
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
|
||||
saveBtn: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', marginTop: 25, elevation: 6, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2 }
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
sectionLine: { flex: 1, height: 1.5, marginLeft: 15, opacity: 0.6 },
|
||||
|
||||
empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
|
||||
empresaIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
empresaInfo: { flex: 1, marginLeft: 16, paddingRight: 10 },
|
||||
empresaNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3, marginBottom: 4 },
|
||||
tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
tutorText: { fontSize: 13, fontWeight: '600' },
|
||||
arrowCircle: { width: 34, height: 34, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
emptyText: { fontSize: 15, fontWeight: '700' }
|
||||
});
|
||||
|
||||
export default ListaEmpresasProfessor;
|
||||
@@ -47,20 +47,25 @@ export default function PerfilProfessor() {
|
||||
Animated.delay(3000),
|
||||
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, []);
|
||||
}, [alertOpacity]);
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: '#2390a6',
|
||||
laranja: '#E38E00',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FFF5F5',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
vermelho: '#EF4444',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#38A169',
|
||||
verde: '#10B981',
|
||||
inputFundo: isDarkMode ? '#252525' : '#F8FAFC',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => { carregarPerfil(); }, []);
|
||||
@@ -69,7 +74,6 @@ export default function PerfilProfessor() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 1. Obter a sessão atual de forma limpa
|
||||
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
|
||||
|
||||
if (sessionError || !session) {
|
||||
@@ -77,18 +81,15 @@ export default function PerfilProfessor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Buscar o perfil filtrando pelo ID da sessão e garantindo que o tipo é PROFESSOR
|
||||
// Isso impede que, se a sessão mudar para aluno, os dados apareçam aqui
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', session.user.id)
|
||||
.eq('tipo', 'professor') // Filtro de segurança
|
||||
.eq('tipo', 'professor')
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
// Se for um aluno a tentar aceder a esta página de professor, expulsamos
|
||||
showAlert('Acesso negado ou perfil não encontrado.', 'error');
|
||||
showAlert('Perfil não encontrado.', 'error');
|
||||
await supabase.auth.signOut();
|
||||
router.replace('/');
|
||||
return;
|
||||
@@ -105,6 +106,19 @@ export default function PerfilProfessor() {
|
||||
|
||||
const guardarPerfil = async () => {
|
||||
if (!perfil) return;
|
||||
|
||||
// 🟢 VALIDAÇÃO: Se algum campo estiver vazio ou for apenas espaços, dispara erro
|
||||
if (
|
||||
!perfil.nome?.trim() ||
|
||||
!perfil.telefone?.trim() ||
|
||||
!perfil.residencia?.trim() ||
|
||||
!perfil.n_escola?.trim() ||
|
||||
!perfil.curso?.trim()
|
||||
) {
|
||||
showAlert('Todos os campos são de preenchimento obrigatório.', 'error');
|
||||
return; // Corta a função aqui para não gravar na base de dados
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
@@ -119,7 +133,7 @@ export default function PerfilProfessor() {
|
||||
|
||||
if (error) throw error;
|
||||
setEditando(false);
|
||||
showAlert('Perfil atualizado!', 'success');
|
||||
showAlert('Perfil atualizado com sucesso!', 'success');
|
||||
} catch (error: any) {
|
||||
showAlert('Erro ao gravar dados.', 'error');
|
||||
}
|
||||
@@ -142,107 +156,169 @@ export default function PerfilProfessor() {
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{alertConfig && (
|
||||
{alertConfig ? (
|
||||
<Animated.View style={[
|
||||
styles.alertBar,
|
||||
styles.toastContainer,
|
||||
{
|
||||
opacity: alertOpacity,
|
||||
backgroundColor: alertConfig.type === 'error' ? cores.vermelho : alertConfig.type === 'success' ? cores.verde : cores.azul,
|
||||
top: insets.top + 10
|
||||
}
|
||||
]}>
|
||||
<Ionicons name={alertConfig.type === 'error' ? "alert-circle" : "checkmark-circle"} size={20} color="#fff" />
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
<Ionicons name={alertConfig.type === 'error' ? "warning" : "checkmark-circle"} size={22} color="#fff" />
|
||||
<Text style={styles.toastText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity style={[styles.backBtn, { borderColor: cores.borda }]} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.azul} />
|
||||
{/* HEADER */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda, backgroundColor: cores.card }]} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>O Meu Perfil</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.editBtn, { backgroundColor: editando ? cores.laranja : cores.card, borderColor: editando ? cores.laranja : cores.borda }]}
|
||||
style={[
|
||||
styles.btnAction,
|
||||
{
|
||||
backgroundColor: editando ? cores.laranja : cores.card,
|
||||
borderColor: editando ? cores.laranja : cores.borda
|
||||
}
|
||||
]}
|
||||
onPress={() => editando ? guardarPerfil() : setEditando(true)}
|
||||
>
|
||||
<Ionicons name={editando ? "checkmark-sharp" : "create-outline"} size={20} color={editando ? "#fff" : cores.azul} />
|
||||
<Ionicons name={editando ? "save" : "pencil"} size={20} color={editando ? "#fff" : cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* HEADER DO PERFIL (AVATAR) */}
|
||||
<View style={styles.profileHeader}>
|
||||
<View style={[styles.avatarBorder, { borderColor: cores.azul }]}>
|
||||
<View style={[styles.avatarBorder, { borderColor: cores.azulSuave }]}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
|
||||
<View style={[styles.roleBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.userRole, { color: cores.azul }]}>PROFESSOR • {perfil?.curso}</Text>
|
||||
<View style={[styles.roleBadge, { backgroundColor: cores.laranjaSuave }]}>
|
||||
<Ionicons name="school" size={12} color={cores.laranja} style={{ marginRight: 6 }} />
|
||||
<Text style={[styles.userRole, { color: cores.laranja }]}>Professor • {perfil?.curso || 'EPVC'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<ModernInput label="Nome do Docente" icon="person" value={perfil?.nome || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} />
|
||||
<View style={styles.cardsContainer}>
|
||||
|
||||
{/* CARTÃO: DADOS PESSOAIS */}
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="person" title="Dados Pessoais" cores={cores} />
|
||||
|
||||
<ModernInput label="Nome" icon="text" value={perfil?.nome || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} />
|
||||
|
||||
<ModernInput label="Email" icon="mail" value={perfil?.email || ''} editable={false} cores={cores} />
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: 12 }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ModernInput label="Contacto" icon="call" value={perfil?.telefone || ''} editable={editando} keyboardType="phone-pad"
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)} cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ModernInput label="Curso Associado" icon="school" value={perfil?.curso || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} />
|
||||
<ModernInput label="Morada" icon="home" value={perfil?.residencia || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, residencia: v } : null)} cores={cores} />
|
||||
</View>
|
||||
|
||||
<ModernInput label="Email de Login" icon="mail" value={perfil?.email || ''} editable={false} cores={cores} />
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<ModernInput label="Nº Mecanográfico" icon="id-card" value={perfil?.n_escola || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} />
|
||||
{/* CARTÃO: DADOS PROFISSIONAIS */}
|
||||
<View style={[styles.sectionCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SectionHeader icon="library" title="Dados Profissionais" cores={cores} />
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: 12 }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ModernInput label="Nº de Professor" icon="id-card" value={perfil?.n_escola || ''} editable={editando} keyboardType="numeric"
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} />
|
||||
</View>
|
||||
<View style={{ flex: 1.5 }}>
|
||||
<ModernInput label="Contacto" icon="call" value={perfil?.telefone || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} />
|
||||
<ModernInput label="Curso" icon="briefcase" value={perfil?.curso || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BOTÕES DE AÇÃO (SEGURANÇA E SAIR) */}
|
||||
<View style={styles.actionsContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push('/Professor/redefenirsenha2')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="lock-closed" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar palavra-passe</Text>
|
||||
<Ionicons name="chevron-forward" size={20} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={terminarSessao}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out" size={22} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar sessão</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* BOTÃO DE CANCELAR EDIÇÃO */}
|
||||
{editando ? (
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setEditando(false); carregarPerfil(); }}>
|
||||
<Text style={[styles.cancelText, { color: cores.secundario }]}>Cancelar e Reverter Alterações</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
</View>
|
||||
|
||||
<View style={styles.actionsContainer}>
|
||||
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push('/Professor/redefenirsenha2')}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.texto }]}>Segurança da Conta</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={terminarSessao}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="power" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.vermelho }]}>Sair do Sistema</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{editando && (
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setEditando(false); carregarPerfil(); }}>
|
||||
<Text style={[styles.cancelText, { color: cores.laranja }]}>Reverter Alterações</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// COMPONENTE HEADER DE SECÇÃO
|
||||
const SectionHeader = ({ icon, title, cores }: any) => (
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={16} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.sectionTitleTxt, { color: cores.texto }]}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
// COMPONENTE DE INPUT MODERNO
|
||||
const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<View style={[styles.inputContainer, { backgroundColor: editable ? cores.fundo : cores.azulSuave, borderColor: editable ? cores.laranja : 'transparent' }]}>
|
||||
<Ionicons name={icon} size={18} color={editable ? cores.laranja : cores.azul} style={{ marginRight: 12 }} />
|
||||
<TextInput {...props} editable={editable} style={[styles.textInput, { color: cores.texto }]} />
|
||||
<View style={[
|
||||
styles.inputContainer,
|
||||
{
|
||||
backgroundColor: editable ? cores.inputFundo : cores.fundo,
|
||||
borderColor: editable ? cores.azul : cores.borda,
|
||||
borderWidth: editable ? 1.5 : 1,
|
||||
opacity: editable ? 1 : 0.7
|
||||
}
|
||||
]}>
|
||||
<Ionicons name={icon} size={18} color={editable ? cores.azul : cores.secundario} style={{ marginRight: 12 }} />
|
||||
<TextInput
|
||||
{...props}
|
||||
editable={editable}
|
||||
style={[styles.textInput, { color: cores.texto }]}
|
||||
placeholderTextColor={cores.secundario}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -250,30 +326,48 @@ const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 },
|
||||
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, fontSize: 13 },
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
editBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1, elevation: 2 },
|
||||
topTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
scrollContent: { paddingHorizontal: 24, paddingBottom: 50 },
|
||||
profileHeader: { alignItems: 'center', marginVertical: 35 },
|
||||
avatarBorder: { padding: 4, borderRadius: 100, borderWidth: 2, position: 'relative' },
|
||||
avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center' },
|
||||
|
||||
// TOAST
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, padding: 16, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#fff', fontWeight: '800', marginLeft: 12, fontSize: 14, flex: 1 },
|
||||
|
||||
// HEADER SUPERIOR
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginTop: 2 },
|
||||
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 50 },
|
||||
|
||||
// PERFIL HIGHLIGHT
|
||||
profileHeader: { alignItems: 'center', marginVertical: 25 },
|
||||
avatarBorder: { padding: 6, borderRadius: 100, borderWidth: 2 },
|
||||
avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', elevation: 4, shadowColor: '#2390a6', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
|
||||
avatarLetter: { color: '#fff', fontSize: 36, fontWeight: '900' },
|
||||
userName: { fontSize: 24, fontWeight: '900', marginTop: 15, letterSpacing: -0.5 },
|
||||
roleBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 },
|
||||
userRole: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
card: { borderRadius: 28, padding: 24, marginBottom: 20, borderWidth: 1 },
|
||||
inputWrapper: { marginBottom: 18 },
|
||||
inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
roleBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, marginTop: 8 },
|
||||
userRole: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
|
||||
cardsContainer: { gap: 20 },
|
||||
|
||||
// CARTÕES DE SECÇÃO
|
||||
sectionCard: { borderRadius: 24, padding: 20, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
iconCircle: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 10 },
|
||||
sectionTitleTxt: { fontSize: 16, fontWeight: '900', letterSpacing: -0.2 },
|
||||
|
||||
// INPUTS
|
||||
inputWrapper: { marginBottom: 16 },
|
||||
inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 52, borderRadius: 16 },
|
||||
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
|
||||
row: { flexDirection: 'row' },
|
||||
actionsContainer: { gap: 12 },
|
||||
menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1 },
|
||||
|
||||
// ACÇÕES E MENU
|
||||
actionsContainer: { gap: 12, marginTop: 10 },
|
||||
menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1, elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.02, shadowRadius: 5 },
|
||||
menuIcon: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
menuText: { flex: 1, marginLeft: 15, fontSize: 15, fontWeight: '800' },
|
||||
cancelBtn: { marginTop: 25, alignItems: 'center' },
|
||||
|
||||
cancelBtn: { marginTop: 20, alignItems: 'center', paddingVertical: 10 },
|
||||
cancelText: { fontSize: 14, fontWeight: '800', textDecorationLine: 'underline' }
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// app/Professor/ProfessorMenu.tsx
|
||||
// app/Professor/ProfessorHome.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -18,25 +18,26 @@ import { useTheme } from '../../themecontext';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
export default function ProfessorMenu() {
|
||||
export default function ProfessorHome() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [nome, setNome] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Paleta EPVC Extraída da Imagem
|
||||
// Paleta EPVC
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Fundo branco limpo como o login
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D', // Azul escuro para contraste
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}), [isDarkMode]);
|
||||
|
||||
@@ -69,105 +70,173 @@ export default function ProfessorMenu() {
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.content,
|
||||
{ paddingBottom: insets.bottom + 20 }
|
||||
{ paddingBottom: insets.bottom + 40 }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{/* HEADER EPVC STYLE */}
|
||||
{/* Cabeçalho */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerRow}>
|
||||
<View>
|
||||
<Text style={[styles.welcome, { color: cores.laranja }]}>Olá, Professor</Text>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.badgeEpvc}>
|
||||
<Ionicons name="school" size={12} color={cores.azul} />
|
||||
<Text style={[styles.badgeTxt, { color: cores.azul }]}>Escola Profissional de Vila do Conde</Text>
|
||||
</View>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={cores.azul} style={{ marginTop: 8, alignSelf: 'flex-start' }} />
|
||||
<ActivityIndicator size="small" color={cores.laranja} style={{ marginTop: 8, alignSelf: 'flex-start' }} />
|
||||
) : (
|
||||
<Text style={[styles.name, { color: cores.texto }]}>{nome || 'Docente'}</Text>
|
||||
<Text style={[styles.name, { color: cores.texto }]} numberOfLines={1}>
|
||||
Olá, {nome ? nome.split(' ')[0] : 'Professor'}
|
||||
</Text>
|
||||
)}
|
||||
<Text style={[styles.subtitle, { color: cores.textoSecundario }]}>Controlo de Estágios Escolares</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/Professor/PerfilProf')}
|
||||
style={[styles.avatarMini, { backgroundColor: cores.azul }]}
|
||||
style={[styles.avatarMini, { backgroundColor: cores.azulSuave, borderColor: cores.azul }]}
|
||||
>
|
||||
<Text style={styles.avatarTxt}>{nome?.charAt(0).toUpperCase() || 'P'}</Text>
|
||||
<Text style={[styles.avatarTxt, { color: cores.azul }]}>{nome?.charAt(0).toUpperCase() || 'P'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.infoBanner, { backgroundColor: cores.azulSuave, borderColor: cores.azul }]}>
|
||||
<Ionicons name="school-outline" size={18} color={cores.azul} />
|
||||
<Text style={[styles.bannerTxt, { color: cores.azul }]}>Escola Profissional de Vila do Conde</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* GRID DE MENU */}
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="document-text-outline" title="Sumários" subtitle="Registos" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} />
|
||||
<MenuCard icon="calendar-outline" title="Presenças" subtitle="Controlo" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} />
|
||||
<MenuCard icon="alert-circle-outline" title="Faltas" subtitle="Gestão" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} />
|
||||
<MenuCard icon="people-outline" title="Alunos" subtitle="Turmas" onPress={() => router.push('/Professor/Alunos/ListaAlunos')} cores={cores} />
|
||||
<MenuCard icon="briefcase-outline" title="Estágios" subtitle="Projetos" onPress={() => router.push('/Professor/Alunos/Estagios')} cores={cores} />
|
||||
<MenuCard icon="business-outline" title="Empresas" subtitle="Parcerias" onPress={() => router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} />
|
||||
<MenuCard icon="settings-outline" title="Definições" subtitle="Sistema" onPress={() => router.push('/Professor/defenicoes2')} cores={cores} />
|
||||
<MenuCard icon="person-outline" title="Perfil" subtitle="A minha conta" onPress={() => router.push('/Professor/PerfilProf')} cores={cores} />
|
||||
{/* Criar Registo */}
|
||||
<TouchableOpacity
|
||||
style={[styles.heroCard, { backgroundColor: cores.azul }]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => router.push('/Professor/Alunos/CriarRegisto')}
|
||||
>
|
||||
<Ionicons name="add-circle" size={120} color="#FFF" style={styles.heroWatermark} />
|
||||
<View style={styles.heroContent}>
|
||||
<View>
|
||||
<Text style={styles.heroTitle}>Novo Registo</Text>
|
||||
<Text style={styles.heroSubtitle}>Adicionar Empresa, Aluno ou Professor</Text>
|
||||
</View>
|
||||
<View style={styles.heroBtn}>
|
||||
<Ionicons name="arrow-forward" size={20} color={cores.azul} />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* SECÇÃO: Controlo Diário */}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.textoSecundario }]}>Controlo diário dos alunos</Text>
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="calendar" title="Presenças" subtitle="Verifica a presença e a localização dos alunos" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} corDestaque={cores.azul} />
|
||||
<MenuCard icon="document-text" title="Sumários" subtitle="Verifica os sumários dos alunos" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} corDestaque={cores.laranja} />
|
||||
<MenuCard icon="alert-circle" title="Faltas" subtitle="Verifica as faltas e as justificações" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} corDestaque="#EF4444" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* SECÇÃO: Empresas */}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.textoSecundario }]}>Entidades</Text>
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="people" title="Alunos" subtitle="Verifica e altera informações de todos os alunos" onPress={() => router.push('/Professor/Alunos/ListaAlunos')} cores={cores} corDestaque={cores.azul} fullWidth />
|
||||
<MenuCard icon="briefcase" title="Estágios" subtitle="Cria novos estágios e edita" onPress={() => router.push('/Professor/Alunos/Estagios')} cores={cores} corDestaque={cores.laranja} />
|
||||
<MenuCard icon="business" title="Empresas" subtitle="Verifica as empresas e altera dados" onPress={() => router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} corDestaque={cores.azul} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* SECÇÃO: Sistema */}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.textoSecundario }]}>Sistema</Text>
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="settings" title="Definições" subtitle="Tema & Contactos" onPress={() => router.push('/Professor/defenicoes2')} cores={cores} corDestaque={cores.textoSecundario} />
|
||||
<MenuCard icon="person" title="Perfil" subtitle="Verifica e altera os teus dados" onPress={() => router.push('/Professor/PerfilProf')} cores={cores} corDestaque={cores.textoSecundario} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.footerLine, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>Estágios+ • EPVC 2026</Text>
|
||||
<Ionicons name="infinite-outline" size={24} color={cores.borda} style={{ marginBottom: 5 }} />
|
||||
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>Estágios+ • Versão 26.5.4</Text>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuCard({ icon, title, subtitle, onPress, cores }: any) {
|
||||
// COMPONENTE DE CARTÃO REFORMULADO
|
||||
function MenuCard({ icon, title, subtitle, onPress, cores, corDestaque, fullWidth = false }: any) {
|
||||
const { isDarkMode } = useTheme(); // CORREÇÃO APLICADA AQUI
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: cores.card,
|
||||
borderColor: cores.borda,
|
||||
width: fullWidth ? '100%' : (width - 60) / 2, // Se for fullWidth ocupa tudo, senão divide
|
||||
}
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={24} color={cores.azul} />
|
||||
{/* Marca de água no fundo para design premium */}
|
||||
<Ionicons name={icon} size={80} color={corDestaque} style={[styles.cardWatermark, { opacity: isDarkMode ? 0.05 : 0.03 }]} />
|
||||
|
||||
<View style={[styles.iconWrapper, { backgroundColor: corDestaque + '15' }]}>
|
||||
<Ionicons name={icon} size={22} color={corDestaque} />
|
||||
</View>
|
||||
<View>
|
||||
|
||||
<View style={styles.cardTextContainer}>
|
||||
<Text style={[styles.cardTitle, { color: cores.texto }]}>{title}</Text>
|
||||
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]}>{subtitle}</Text>
|
||||
</View>
|
||||
<View style={[styles.arrowCircle, { backgroundColor: cores.laranja }]}>
|
||||
<Ionicons name="chevron-forward" size={12} color="#FFF" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: { padding: 24 },
|
||||
header: { marginBottom: 35 },
|
||||
headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
welcome: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 },
|
||||
name: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8, marginTop: 2 },
|
||||
avatarMini: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 8 },
|
||||
avatarTxt: { color: '#fff', fontSize: 22, fontWeight: '900' },
|
||||
infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 16, borderWidth: 1, borderStyle: 'dashed' },
|
||||
bannerTxt: { fontSize: 13, fontWeight: '700', marginLeft: 10 },
|
||||
content: { padding: 20 },
|
||||
|
||||
// Header
|
||||
header: { marginBottom: 25, marginTop: 10 },
|
||||
headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
badgeEpvc: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#E0F2F4', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 8 },
|
||||
badgeTxt: { fontSize: 10, fontWeight: '900', letterSpacing: 1, marginLeft: 4 },
|
||||
name: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 },
|
||||
subtitle: { fontSize: 14, fontWeight: '500', marginTop: 2 },
|
||||
avatarMini: { width: 56, height: 56, borderRadius: 20, justifyContent: 'center', alignItems: 'center', borderWidth: 2 },
|
||||
avatarTxt: { fontSize: 24, fontWeight: '900' },
|
||||
|
||||
// Hero Card (Novo Registo)
|
||||
heroCard: { borderRadius: 28, padding: 24, minHeight: 140, justifyContent: 'flex-end', overflow: 'hidden', elevation: 8, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, marginBottom: 30 },
|
||||
heroWatermark: { position: 'absolute', right: -20, top: -20, opacity: 0.2, transform: [{ rotate: '15deg' }] },
|
||||
heroContent: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' },
|
||||
heroTitle: { color: '#FFF', fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
heroSubtitle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 13, fontWeight: '600', marginTop: 4 },
|
||||
heroBtn: { width: 44, height: 44, backgroundColor: '#FFF', borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
// Secções e Grid
|
||||
sectionContainer: { marginBottom: 25 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12, marginLeft: 4 },
|
||||
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
|
||||
|
||||
// Cartões Menores
|
||||
card: {
|
||||
width: (width - 64) / 2,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
padding: 18,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
overflow: 'hidden',
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.03,
|
||||
shadowRadius: 15,
|
||||
elevation: 3,
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 10,
|
||||
shadowOffset: { width: 0, height: 4 }
|
||||
},
|
||||
iconBox: { width: 48, height: 48, borderRadius: 15, justifyContent: 'center', alignItems: 'center', marginBottom: 16 },
|
||||
cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.2 },
|
||||
cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600', opacity: 0.7 },
|
||||
arrowCircle: { position: 'absolute', top: 20, right: 15, width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' },
|
||||
footer: { marginTop: 30, alignItems: 'center' },
|
||||
footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 },
|
||||
cardWatermark: { position: 'absolute', right: -15, bottom: -15, transform: [{ rotate: '-10deg' }] },
|
||||
iconWrapper: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginBottom: 16 },
|
||||
cardTextContainer: { marginTop: 'auto' },
|
||||
cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600' },
|
||||
|
||||
// Footer
|
||||
footer: { marginTop: 20, alignItems: 'center', paddingBottom: 20 },
|
||||
footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 }
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// app/Definicoes.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Linking,
|
||||
@@ -23,34 +23,38 @@ const Definicoes = memo(() => {
|
||||
const { isDarkMode, toggleTheme } = useTheme();
|
||||
const [notificacoes, setNotificacoes] = useState(true);
|
||||
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
// TOAST ANIMADO UNIVERSAL
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
setAlertConfig({ msg, type });
|
||||
Animated.sequence([
|
||||
Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
|
||||
Animated.delay(2500),
|
||||
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, []);
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
// Cores EPVC
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Branco puro do login
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA', // Novo design Premium
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: '#FFF5F5',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#38A169',
|
||||
vermelho: erroCor,
|
||||
verde: sucessoCor,
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -58,40 +62,43 @@ const Definicoes = memo(() => {
|
||||
await supabase.auth.signOut();
|
||||
router.replace('/');
|
||||
} catch (e) {
|
||||
showAlert("Erro ao sair da conta", "error");
|
||||
showToast("Erro ao sair da conta", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const abrirURL = (url: string) => {
|
||||
Linking.canOpenURL(url).then(supported => {
|
||||
if (supported) Linking.openURL(url);
|
||||
else showAlert("Não foi possível abrir o link", "error");
|
||||
else showToast("Não foi possível abrir o link", "error");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{alertConfig && (
|
||||
<Animated.View style={[
|
||||
styles.alertBar,
|
||||
{
|
||||
opacity: alertOpacity,
|
||||
backgroundColor: alertConfig.type === 'error' ? cores.vermelho : alertConfig.type === 'success' ? cores.verde : cores.azul,
|
||||
top: insets.top + 10
|
||||
}
|
||||
]}>
|
||||
<Ionicons name={alertConfig.type === 'error' ? "alert-circle" : "information-circle"} size={20} color="#fff" />
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
{/* 🟢 TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: cores.vermelho } :
|
||||
toast.type === 'success' ? { backgroundColor: cores.verde } :
|
||||
{ backgroundColor: cores.azul }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={toast.type === 'error' ? "warning" : toast.type === 'success' ? "checkmark-circle" : "information-circle"}
|
||||
size={24}
|
||||
color="#FFF"
|
||||
/>
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER MODERNO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnVoltar, { borderColor: cores.borda }]}
|
||||
style={[styles.btnVoltar, { borderColor: cores.borda, backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
@@ -100,7 +107,7 @@ const Definicoes = memo(() => {
|
||||
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
|
||||
<Text style={[styles.tituloSub, { color: cores.laranja }]}>Sistema & Suporte</Text>
|
||||
</View>
|
||||
<View style={{ width: 45 }} />
|
||||
<View style={{ width: 44 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
@@ -116,9 +123,9 @@ const Definicoes = memo(() => {
|
||||
value={notificacoes}
|
||||
onValueChange={(v) => {
|
||||
setNotificacoes(v);
|
||||
showAlert(v ? "Notificações ligadas" : "Notificações desligadas", "info");
|
||||
showToast(v ? "Notificações ligadas" : "Notificações desligadas", "info");
|
||||
}}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.laranja }}
|
||||
trackColor={{ false: cores.borda, true: cores.laranja }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
@@ -131,7 +138,7 @@ const Definicoes = memo(() => {
|
||||
<Switch
|
||||
value={isDarkMode}
|
||||
onValueChange={toggleTheme}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.laranja }}
|
||||
trackColor={{ false: cores.borda, true: cores.laranja }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
@@ -150,7 +157,7 @@ const Definicoes = memo(() => {
|
||||
<SettingLink
|
||||
icon="mail-unread-outline"
|
||||
label="Secretaria"
|
||||
subLabel="Atendimento ao Docente"
|
||||
subLabel="secretaria@epvc.pt"
|
||||
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
|
||||
cores={cores}
|
||||
showBorder
|
||||
@@ -173,7 +180,7 @@ const Definicoes = memo(() => {
|
||||
<Text style={[styles.versionBadge, { color: cores.azul, backgroundColor: cores.azulSuave }]}>2.6.17</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.item} onPress={handleLogout}>
|
||||
<TouchableOpacity style={styles.item} onPress={handleLogout} activeOpacity={0.7}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
@@ -198,6 +205,7 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any)
|
||||
<TouchableOpacity
|
||||
style={[styles.item, showBorder && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={20} color={cores.azul} />
|
||||
@@ -206,7 +214,7 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any)
|
||||
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>{label}</Text>
|
||||
<Text style={{ color: cores.secundario, fontSize: 12, fontWeight: '600' }}>{subLabel}</Text>
|
||||
</View>
|
||||
<View style={[styles.arrowCircle, { borderColor: cores.borda }]}>
|
||||
<View style={[styles.arrowCircle, { borderColor: cores.borda, backgroundColor: cores.fundo }]}>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -214,23 +222,28 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 },
|
||||
alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1, fontSize: 13 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12, flex: 1 },
|
||||
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
btnVoltar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
tituloSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
scrollContent: { paddingHorizontal: 24, paddingBottom: 60 },
|
||||
sectionLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, marginLeft: 4, letterSpacing: 1 },
|
||||
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10, elevation: 2 },
|
||||
tituloGeral: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
tituloSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 2 },
|
||||
|
||||
scrollContent: { paddingHorizontal: 20, paddingTop: 10, paddingBottom: 60 },
|
||||
sectionLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 0.8 },
|
||||
|
||||
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 8 },
|
||||
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 18 },
|
||||
iconContainer: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 },
|
||||
versionBadge: { fontSize: 11, fontWeight: '900', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
iconContainer: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
versionBadge: { fontSize: 12, fontWeight: '900', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 },
|
||||
arrowCircle: { width: 34, height: 34, borderRadius: 12, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
footer: { alignItems: 'center', marginTop: 50 },
|
||||
footerLine: { width: 30, height: 3, borderRadius: 2, marginBottom: 15 },
|
||||
footerText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }
|
||||
footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 },
|
||||
footerText: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }
|
||||
});
|
||||
|
||||
export default Definicoes;
|
||||
@@ -1,9 +1,10 @@
|
||||
// app/forgot-password.tsx
|
||||
// app/Professor/redefenirsenha2.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
@@ -14,297 +15,227 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// ESTADOS PARA OS AVISOS MODERNOS
|
||||
const [status, setStatus] = useState<{ type: 'error' | 'success'; msg: string } | null>(null);
|
||||
|
||||
export default function RedefinirSenhaInterna() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const handleSendResetEmail = async () => {
|
||||
setStatus(null); // Limpa avisos anteriores
|
||||
const [novaPassword, setNovaPassword] = useState('');
|
||||
const [confirmarPassword, setConfirmarPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isFocused1, setIsFocused1] = useState(false);
|
||||
const [isFocused2, setIsFocused2] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
if (!email) {
|
||||
setStatus({ type: 'error', msg: 'Por favor, insira o seu email corretamente.' });
|
||||
// ESTADOS PARA O TOAST ANIMADO
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' }>({ visible: false, message: '', type: 'error' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
const erroCor = '#EF4444';
|
||||
const sucessoCor = '#10B981';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
placeholder: isDarkMode ? '#555' : '#94A3B8',
|
||||
verde: sucessoCor // 🟢 COR ADICIONADA AQUI
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' = 'error') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'error' }));
|
||||
}, 3500);
|
||||
});
|
||||
}, [insets.top, slideAnim]);
|
||||
|
||||
const handleUpdatePassword = async () => {
|
||||
if (!novaPassword || !confirmarPassword) {
|
||||
showToast("Preenche todos os campos!", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (novaPassword.length < 6) {
|
||||
showToast("A palavra-passe tem de ter pelo menos 6 caracteres.", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (novaPassword !== confirmarPassword) {
|
||||
showToast("As palavras-passe não coincidem.", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email);
|
||||
const { error } = await supabase.auth.updateUser({ password: novaPassword });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setStatus({ type: 'success', msg: 'Link enviado! Verifique ao seu email.' });
|
||||
showToast("Palavra-passe atualizada com segurança!", 'success');
|
||||
|
||||
// Espera 3 segundos para o utilizador ler e volta para o login
|
||||
setTimeout(() => router.back(), 3500);
|
||||
setTimeout(() => router.back(), 2000);
|
||||
|
||||
} catch (err: any) {
|
||||
setStatus({ type: 'error', msg: 'Não foi possível enviar o email. Tente novamente!' });
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.mainContainer}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButtonTop}
|
||||
>
|
||||
<View style={styles.backIconCircle}>
|
||||
<Ionicons name="arrow-back" size={20} color="#1E293B" />
|
||||
</View>
|
||||
<Text style={styles.backButtonText}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* TOAST ANIMADO NO TOPO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
toast.type === 'error' ? { backgroundColor: erroCor } : { backgroundColor: sucessoCor }
|
||||
]}>
|
||||
<Ionicons name={toast.type === 'error' ? "warning" : "shield-checkmark"} size={24} color="#FFF" />
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconWrapper}>
|
||||
<View style={styles.iconCircle}>
|
||||
<Ionicons name="mail-unread-outline" size={38} color="#2390a6" />
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { borderColor: cores.borda, backgroundColor: cores.card }]}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Segurança</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Estágios+</Text>
|
||||
</View>
|
||||
<View style={{ width: 44 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
|
||||
|
||||
<View style={styles.heroSection}>
|
||||
<View style={[styles.iconShield, { backgroundColor: cores.azulSuave, borderColor: cores.azul }]}>
|
||||
<Ionicons name="lock-closed" size={42} color={cores.azul} />
|
||||
<View style={[styles.badgeVerificado, { backgroundColor: cores.verde }]}>
|
||||
<Ionicons name="checkmark" size={12} color="#FFF" />
|
||||
</View>
|
||||
<View style={styles.iconBadge} />
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>Não sabes a tua palavra-passe?</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Não te preocupes! Insere o teu email e enviaremos instruções para recuperares o acesso à nossa app.
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Alterar Palavra-passe</Text>
|
||||
<Text style={[styles.subtitle, { color: cores.secundario }]}>
|
||||
Cria uma palavra-passe forte para manteres a tua conta protegida.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* AVISOS/ERROS MODERNOS AQUI */}
|
||||
{status && (
|
||||
<View style={[styles.statusBanner, status.type === 'success' ? styles.successBg : styles.errorBg]}>
|
||||
<Ionicons
|
||||
name={status.type === 'success' ? "checkmark-circle" : "alert-circle"}
|
||||
size={20}
|
||||
color={status.type === 'success' ? "#059669" : "#EF4444"}
|
||||
/>
|
||||
<Text style={[styles.statusText, status.type === 'success' ? styles.successText : styles.errorText]}>
|
||||
{status.msg}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.form}>
|
||||
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.label, isFocused && { color: '#2390a6' }]}>Email</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
isFocused && styles.inputFocused
|
||||
]}
|
||||
placeholder="Insira o seu email"
|
||||
placeholderTextColor="#94A3B8"
|
||||
value={email}
|
||||
onChangeText={(val) => {
|
||||
setEmail(val);
|
||||
if(status) setStatus(null); // Limpa o erro ao começar a digitar
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!loading}
|
||||
/>
|
||||
<Text style={[styles.label, { color: cores.secundario }, isFocused1 && { color: cores.azul }]}>Nova Palavra-passe</Text>
|
||||
<View style={[
|
||||
styles.inputContainer,
|
||||
{ backgroundColor: cores.inputFundo, borderColor: isFocused1 ? cores.azul : cores.borda }
|
||||
]}>
|
||||
<Ionicons name="key-outline" size={20} color={isFocused1 ? cores.azul : cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: cores.texto }]}
|
||||
placeholder="Mínimo de 6 caracteres"
|
||||
placeholderTextColor={cores.placeholder}
|
||||
value={novaPassword}
|
||||
onChangeText={setNovaPassword}
|
||||
onFocus={() => setIsFocused1(true)}
|
||||
onBlur={() => setIsFocused1(false)}
|
||||
secureTextEntry={!showPassword}
|
||||
editable={!loading}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={{ padding: 5 }}>
|
||||
<Ionicons name={showPassword ? "eye-off-outline" : "eye-outline"} size={20} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.label, { color: cores.secundario }, isFocused2 && { color: cores.azul }]}>Confirmar Palavra-passe</Text>
|
||||
<View style={[
|
||||
styles.inputContainer,
|
||||
{ backgroundColor: cores.inputFundo, borderColor: isFocused2 ? cores.azul : cores.borda }
|
||||
]}>
|
||||
<Ionicons name="checkmark-done-outline" size={20} color={isFocused2 ? cores.azul : cores.secundario} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: cores.texto }]}
|
||||
placeholder="Repete a tua nova palavra-passe"
|
||||
placeholderTextColor={cores.placeholder}
|
||||
value={confirmarPassword}
|
||||
onChangeText={setConfirmarPassword}
|
||||
onFocus={() => setIsFocused2(true)}
|
||||
onBlur={() => setIsFocused2(false)}
|
||||
secureTextEntry={!showPassword}
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleSendResetEmail}
|
||||
style={[styles.button, { backgroundColor: loading ? cores.secundario : cores.azul }]}
|
||||
onPress={handleUpdatePassword}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Enviar Instruções</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Ionicons name="shield-checkmark" size={20} color="#FFF" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.buttonText}>Atualizar Palavra-passe</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>EPVC Estágios+ • 2026</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 28,
|
||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 20 : 60,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
backButtonTop: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: 30,
|
||||
},
|
||||
backIconCircle: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#F8FAFC',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: '#F1F5F9',
|
||||
},
|
||||
backButtonText: {
|
||||
fontSize: 15,
|
||||
color: '#64748B',
|
||||
fontWeight: '600',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 35,
|
||||
},
|
||||
iconWrapper: {
|
||||
position: 'relative',
|
||||
marginBottom: 20,
|
||||
},
|
||||
iconCircle: {
|
||||
width: 90,
|
||||
height: 90,
|
||||
backgroundColor: '#F0F7FF',
|
||||
borderRadius: 30,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#DBEAFE',
|
||||
},
|
||||
iconBadge: {
|
||||
position: 'absolute',
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#2390a6',
|
||||
borderWidth: 3,
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
color: '#0F172A',
|
||||
textAlign: 'center',
|
||||
letterSpacing: -1,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 15,
|
||||
color: '#64748B',
|
||||
textAlign: 'center',
|
||||
marginTop: 10,
|
||||
lineHeight: 22,
|
||||
maxWidth: 300,
|
||||
},
|
||||
// ESTILOS DOS BANNERS MODERNOS
|
||||
statusBanner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
marginBottom: 25,
|
||||
borderWidth: 1,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 10,
|
||||
flex: 1,
|
||||
},
|
||||
errorBg: { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' },
|
||||
errorText: { color: '#B91C1C' },
|
||||
successBg: { backgroundColor: '#F0FDF4', borderColor: '#DCFCE7' },
|
||||
successText: { color: '#166534' },
|
||||
safe: { flex: 1 },
|
||||
// TOAST STYLES
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 100, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 10, flex: 1 },
|
||||
|
||||
form: {
|
||||
width: '100%',
|
||||
},
|
||||
inputWrapper: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#475569',
|
||||
marginBottom: 10,
|
||||
marginLeft: 4,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#FBFDFF',
|
||||
borderRadius: 18,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
fontSize: 16,
|
||||
color: '#0F172A',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#F1F5F9',
|
||||
},
|
||||
inputFocused: {
|
||||
borderColor: '#2390a6',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#dd8707',
|
||||
borderRadius: 18,
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#dd8707',
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 10,
|
||||
elevation: 6,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#E2E8F0',
|
||||
elevation: 0,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 40,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 12,
|
||||
color: '#CBD5E1',
|
||||
fontWeight: '600',
|
||||
},
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
backBtn: { width: 44, height: 44, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginTop: 2 }, // 🟢 ESTILO ADICIONADO AQUI
|
||||
|
||||
scrollContent: { paddingHorizontal: 28, paddingBottom: 40 },
|
||||
|
||||
heroSection: { alignItems: 'center', marginTop: 30, marginBottom: 40 },
|
||||
iconShield: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5, position: 'relative' },
|
||||
badgeVerificado: { position: 'absolute', bottom: -5, right: -5, width: 24, height: 24, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: '#FFF' },
|
||||
title: { fontSize: 26, fontWeight: '900', marginTop: 20, letterSpacing: -0.5 },
|
||||
subtitle: { fontSize: 14, textAlign: 'center', marginTop: 8, lineHeight: 22, fontWeight: '500' },
|
||||
|
||||
form: { width: '100%' },
|
||||
inputWrapper: { marginBottom: 20 },
|
||||
label: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
input: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
|
||||
button: { borderRadius: 18, height: 60, justifyContent: 'center', alignItems: 'center', marginTop: 15, elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
buttonText: { color: '#fff', fontSize: 15, fontWeight: '900', letterSpacing: 1 },
|
||||
});
|
||||
@@ -28,6 +28,7 @@ export default function LoginScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vai à base de dados perguntar quem é esta pessoa
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('tipo')
|
||||
@@ -44,10 +45,13 @@ export default function LoginScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
// O Polícia Sinaleiro: Manda cada um para o seu lado
|
||||
if (data.tipo === 'professor') {
|
||||
router.replace('/Professor/ProfessorHome');
|
||||
} else if (data.tipo === 'aluno') {
|
||||
router.replace('/Aluno/AlunoHome');
|
||||
} else if (data.tipo === 'empresa') {
|
||||
router.replace('/Empresas/EmpresaHome'); // 🟢 Rota da empresa adicionada!
|
||||
} else {
|
||||
Alert.alert('Erro', 'Tipo de conta inválido');
|
||||
}
|
||||
@@ -123,14 +127,14 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 20, // Reduzi para o bloco de texto ficar mais perto do Auth
|
||||
marginBottom: 20,
|
||||
},
|
||||
logoContainer: {
|
||||
width: 420,
|
||||
height: 220,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: -60, // MARGEM NEGATIVA: Aproxima o texto da imagem ao máximo
|
||||
marginBottom: -60,
|
||||
},
|
||||
logoImage: {
|
||||
width: '100%',
|
||||
@@ -149,7 +153,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
color: '#64748B',
|
||||
textAlign: 'center',
|
||||
marginTop: 2, // Reduzi para o subtítulo ficar colado ao título principal
|
||||
marginTop: 2,
|
||||
fontWeight: '500',
|
||||
lineHeight: 24,
|
||||
maxWidth: 280,
|
||||
|
||||
Reference in New Issue
Block a user