atualizacoess

This commit is contained in:
2026-03-18 12:40:11 +00:00
parent ff2b3fd8e7
commit f61baaf134
15 changed files with 1920 additions and 1743 deletions

View File

@@ -1,6 +1,5 @@
// app/Aluno/AlunoHome.tsx
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useFocusEffect } from '@react-navigation/native';
import { decode } from 'base64-arraybuffer';
import * as DocumentPicker from 'expo-document-picker';
@@ -11,7 +10,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
Animated,
Linking,
Modal,
Platform,
SafeAreaView,
ScrollView,
@@ -26,6 +25,7 @@ import { Calendar, LocaleConfig } from 'react-native-calendars';
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
// Configuração PT-PT para o Calendário
LocaleConfig.locales['pt'] = {
monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
@@ -48,34 +48,22 @@ const AlunoHome = memo(() => {
const router = useRouter();
const hojeStr = new Date().toISOString().split('T')[0];
// Estados de Dados
const [selectedDate, setSelectedDate] = useState(hojeStr);
const [configEstagio, setConfigEstagio] = useState({ inicio: '2026-01-05', fim: '2026-05-30' });
const [configEstagio, setConfigEstagio] = useState({ inicio: '', fim: '' });
const [presencas, setPresencas] = useState<Record<string, boolean>>({});
const [faltas, setFaltas] = useState<Record<string, boolean>>({});
const [sumarios, setSumarios] = useState<Record<string, string>>({});
const [urlsJustificacao, setUrlsJustificacao] = useState<Record<string, string>>({});
// Estados de UI
const [pdf, setPdf] = useState<any>(null);
const [editandoSumario, setEditandoSumario] = useState(false);
const [isLoadingDB, setIsLoadingDB] = useState(true);
const [isLocating, setIsLocating] = useState(false);
const [isUploading, setIsUploading] = useState(false);
// --- SISTEMA DE AVISOS MODERNOS ---
const [showLocationModal, setShowLocationModal] = useState(false);
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
const alertOpacity = useMemo(() => new Animated.Value(0), []);
const showAlert = (msg: string, type: 'success' | 'error' | 'info' = 'info') => {
setAlertConfig({ msg, type });
Animated.sequence([
Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
Animated.delay(3000),
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
]).start(() => setAlertConfig(null));
};
const azulPetroleo = '#2390a6';
const laranjaEPVC = '#dd8707';
@@ -90,76 +78,80 @@ const AlunoHome = memo(() => {
verde: '#10B981',
}), [isDarkMode]);
useFocusEffect(
useCallback(() => {
carregarConfigLocal();
fetchDadosSupabase();
}, [])
);
const carregarConfigLocal = async () => {
try {
const config = await AsyncStorage.getItem('@dados_estagio');
if (config) setConfigEstagio(JSON.parse(config));
} catch (e) { console.error(e); }
const showAlert = (msg: string, type: 'success' | 'error' | 'info' = 'info') => {
setAlertConfig({ msg, type });
Animated.sequence([
Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
Animated.delay(3000),
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
]).start(() => setAlertConfig(null));
};
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, []));
const fetchDadosSupabase = async () => {
setIsLoadingDB(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: estagio } = await supabase.from('estagios').select('data_inicio, data_fim').eq('aluno_id', user.id).single();
if (estagio) setConfigEstagio({ inicio: estagio.data_inicio, fim: estagio.data_fim });
const { data, error } = await supabase.from('presencas').select('*').eq('aluno_id', user.id);
if (error) throw error;
const p: any = {}, f: any = {}, s: any = {}, u: any = {};
data?.forEach(item => {
if (item.estado === 'presente') { p[item.data] = true; s[item.data] = item.sumario || ''; }
else { f[item.data] = true; u[item.data] = item.justificacao_url || ''; }
});
setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u);
} catch (error: any) {
console.error(error);
} finally {
setIsLoadingDB(false);
}
} catch (error) { console.error(error); }
finally { setIsLoadingDB(false); }
};
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
const infoData = useMemo(() => {
const data = new Date(selectedDate);
const diaSemana = data.getDay();
const diaSemana = data.getDay();
const nomeFeriado = feriadosMap[selectedDate];
const fora = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
const antesDoInicio = configEstagio.inicio && selectedDate < configEstagio.inicio;
const depoisDoFim = configEstagio.fim && selectedDate > configEstagio.fim;
return {
valida: diaSemana !== 0 && diaSemana !== 6 && !fora && !nomeFeriado,
podeMarcar: selectedDate === hojeStr && !fora && !nomeFeriado,
nomeFeriado
valida: diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado,
podeMarcar: selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado,
nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: antesDoInicio || depoisDoFim
};
}, [selectedDate, configEstagio, hojeStr, feriadosMap]);
const handlePresenca = async () => {
if (!infoData.podeMarcar) return showAlert("Não podes marcar presença nesta data.", "error");
const handlePresencaClick = async () => {
const { status } = await Location.getForegroundPermissionsAsync();
if (status === 'granted') {
executarMarcacao();
} else {
setShowLocationModal(true);
}
};
const executarMarcacao = async () => {
setShowLocationModal(false);
setIsLocating(true);
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') throw new Error("Sem permissão de GPS.");
const loc = await Location.getCurrentPositionAsync({});
if (status !== 'granted') throw new Error("Sem acesso ao GPS.");
const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
const { data: { user } } = await supabase.auth.getUser();
const { error } = await supabase.from('presencas').upsert({
await supabase.from('presencas').upsert({
aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude
});
if (error) throw error;
showAlert("Presença registada com sucesso!", "success");
showAlert("Presença marcada com sucesso!", "success");
fetchDadosSupabase();
} catch (e: any) {
showAlert(e.message, "error");
} finally { setIsLocating(false); }
} catch (e: any) { showAlert(e.message, "error"); }
finally { setIsLocating(false); }
};
const handleFalta = async () => {
if (!infoData.valida) return showAlert("Data inválida para registar falta.", "error");
if (infoData.foraDeRange) return showAlert("Fora do período de estágio.", "error");
if (!infoData.valida) return showAlert("Não podes marcar falta neste dia.", "error");
try {
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' });
@@ -168,6 +160,29 @@ const AlunoHome = memo(() => {
} catch (e) { showAlert("Erro ao registar falta.", "error"); }
};
const selecionarDocumento = async () => {
const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
if (!res.canceled) setPdf(res.assets[0]);
};
const enviarJustificativo = async () => {
if (!pdf) return;
setIsUploading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
const fileBase64 = await FileSystem.readAsStringAsync(pdf.uri, { encoding: FileSystem.EncodingType.Base64 });
const fileName = `${user?.id}/${selectedDate}_justificacao.pdf`;
const { error: uploadError } = await supabase.storage.from('justificacoes').upload(fileName, decode(fileBase64), { contentType: 'application/pdf', upsert: true });
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
setPdf(null);
showAlert("Justificativo enviado!", "success");
fetchDadosSupabase();
} catch (e) { showAlert("Erro no upload.", "error"); }
finally { setIsUploading(false); }
};
const guardarSumario = async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
@@ -178,59 +193,54 @@ const AlunoHome = memo(() => {
} catch (e) { showAlert("Erro ao guardar sumário.", "error"); }
};
const enviarJustificacao = async () => {
if (!pdf) return;
setIsUploading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
const fileName = `${user?.id}/${selectedDate}_${Date.now()}.pdf`;
const base64 = await FileSystem.readAsStringAsync(pdf.uri, { encoding: 'base64' });
const { error: upErr } = await supabase.storage.from('justificacoes').upload(fileName, decode(base64), { contentType: 'application/pdf' });
if (upErr) throw upErr;
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
showAlert("Documento enviado!", "success");
setPdf(null);
fetchDadosSupabase();
} catch (e: any) { showAlert(e.message, "error"); }
finally { setIsUploading(false); }
};
const visualizarDocumento = async (url: string) => {
const supported = await Linking.canOpenURL(url);
if (supported) await Linking.openURL(url);
else showAlert("Não foi possível abrir o ficheiro.", "error");
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* ALERT OVERLAY MODERNO */}
<Modal visible={showLocationModal} transparent animationType="slide">
<View style={styles.modalOverlay}>
<View style={[styles.locationModal, { backgroundColor: themeStyles.card }]}>
<View style={styles.modalHandle} />
<View style={[styles.iconCircle, { backgroundColor: azulPetroleo + '15' }]}>
<Ionicons name="location" size={40} color={azulPetroleo} />
</View>
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Validar Localização</Text>
<Text style={[styles.modalDesc, { color: themeStyles.textoSecundario }]}>
Para registar a tua presença, precisamos de confirmar que te encontras no local de estágio.
</Text>
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: azulPetroleo }]} onPress={executarMarcacao}>
<Text style={styles.txtBtn}>Confirmar e Marcar</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnFechar} onPress={() => setShowLocationModal(false)}>
<Text style={[styles.txtFechar, { color: themeStyles.textoSecundario }]}>Agora não</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
{alertConfig && (
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? '#EF4444' : alertConfig.type === 'success' ? '#10B981' : azulPetroleo }]}>
<Ionicons name={alertConfig.type === 'error' ? "alert-circle" : "checkmark-circle"} size={20} color="#fff" />
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? '#EF4444' : azulPetroleo }]}>
<Text style={styles.alertText}>{alertConfig.msg}</Text>
</Animated.View>
)}
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}>
<Ionicons name="person-circle-outline" size={32} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')}>
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
</TouchableOpacity>
<View style={styles.topIcons}>
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')} style={{ marginRight: 15 }}>
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
</TouchableOpacity>
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}>
<Ionicons name="person-circle-outline" size={30} color={themeStyles.texto} />
</TouchableOpacity>
</View>
</View>
<View style={styles.botoesLinha}>
<TouchableOpacity
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
onPress={handlePresenca}
onPress={handlePresencaClick}
disabled={!infoData.podeMarcar || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
>
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
@@ -240,21 +250,22 @@ const AlunoHome = memo(() => {
onPress={handleFalta}
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
>
<Text style={styles.txtBtn}>Faltei</Text>
<Text style={styles.txtBtn}>Marcar Falta</Text>
</TouchableOpacity>
</View>
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
{isLoadingDB && <View style={styles.loaderOverlay}><ActivityIndicator size="large" color={azulPetroleo} /></View>}
<Calendar
key={isDarkMode ? 'dark' : 'light'}
minDate={configEstagio.inicio || undefined}
maxDate={configEstagio.fim || undefined}
theme={{
calendarBackground: themeStyles.card,
dayTextColor: themeStyles.texto,
monthTextColor: themeStyles.texto,
todayTextColor: azulPetroleo,
arrowColor: azulPetroleo,
selectedDayBackgroundColor: azulPetroleo
selectedDayBackgroundColor: azulPetroleo,
textDisabledColor: isDarkMode ? '#333' : '#DDD'
}}
markedDates={{
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
@@ -262,46 +273,48 @@ const AlunoHome = memo(() => {
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#EF4444' } }), {}),
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
}}
onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }}
onDayPress={(day) => setSelectedDate(day.dateString)}
/>
</View>
{presencas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.rowTitle}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
{!editandoSumario && <TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>}
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário do Dia</Text>
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>
</View>
<TextInput
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto }]}
multiline editable={editandoSumario}
value={sumarios[selectedDate]}
onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})}
placeholder="O que fizeste hoje?"
placeholder="Descreve o que fizeste..."
placeholderTextColor="#94A3B8"
/>
{editandoSumario && <TouchableOpacity style={[styles.btnAcao, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar</Text></TouchableOpacity>}
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar Sumário</Text></TouchableOpacity>}
</View>
)}
{faltas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificação</Text>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar Falta</Text>
{urlsJustificacao[selectedDate] ? (
<TouchableOpacity style={[styles.btnVer, { backgroundColor: themeStyles.textoSecundario }]} onPress={() => visualizarDocumento(urlsJustificacao[selectedDate])}>
<Ionicons name="document-text-outline" size={20} color="#fff" />
<Text style={[styles.txtBtn, { marginLeft: 8 }]}>Ver PDF</Text>
</TouchableOpacity>
) : (
<View>
<TouchableOpacity style={[styles.btnAnexar, { borderColor: themeStyles.borda }]} onPress={async () => {
const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
if (!res.canceled) setPdf(res.assets[0]);
}}>
<Text style={{ color: themeStyles.texto }}>{pdf ? pdf.name : 'Selecionar PDF'}</Text>
</TouchableOpacity>
{pdf && <TouchableOpacity style={[styles.btnAcao, { backgroundColor: themeStyles.verde }]} onPress={enviarJustificacao} disabled={isUploading}>{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Enviar</Text>}</TouchableOpacity>}
<View style={styles.justificadoBox}>
<Ionicons name="checkmark-circle" size={20} color={themeStyles.verde} />
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Documento Enviado</Text>
</View>
) : (
<>
<TouchableOpacity style={[styles.btnUpload, { borderColor: azulPetroleo }]} onPress={selecionarDocumento}>
<Ionicons name="document-attach-outline" size={20} color={azulPetroleo} />
<Text style={{ color: azulPetroleo, fontWeight: '600' }}>{pdf ? pdf.name : "Selecionar PDF"}</Text>
</TouchableOpacity>
{pdf && (
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: azulPetroleo }]} onPress={enviarJustificativo} disabled={isUploading}>
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Justificativo</Text>}
</TouchableOpacity>
)}
</>
)}
</View>
)}
@@ -312,24 +325,33 @@ const AlunoHome = memo(() => {
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 999, elevation: 10 },
alertText: { color: '#fff', fontWeight: 'bold', marginLeft: 10, flex: 1 },
container: { padding: 20, paddingBottom: 40 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
title: { fontSize: 22, fontWeight: '800' },
container: { padding: 20 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 25 },
topIcons: { flexDirection: 'row', alignItems: 'center' },
title: { fontSize: 26, fontWeight: '900' },
alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 15, zIndex: 1000 },
alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' },
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
btn: { padding: 16, borderRadius: 16, width: '48%', alignItems: 'center' },
btnAcao: { padding: 14, borderRadius: 12, marginTop: 10, alignItems: 'center' },
btnAnexar: { borderWidth: 1, padding: 14, borderRadius: 12, marginBottom: 10, alignItems: 'center', borderStyle: 'dashed' },
btnVer: { padding: 14, borderRadius: 12, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
btn: { padding: 18, borderRadius: 22, width: '48%', alignItems: 'center', elevation: 3 },
txtBtn: { color: '#fff', fontWeight: '800', fontSize: 14 },
disabled: { opacity: 0.4 },
txtBtn: { color: '#fff', fontWeight: 'bold' },
cardCalendar: { borderRadius: 24, padding: 10, borderWidth: 1, position: 'relative', overflow: 'hidden' },
loaderOverlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.4)', zIndex: 10 },
card: { padding: 20, borderRadius: 24, marginTop: 20, borderWidth: 1 },
cardTitulo: { fontSize: 17, fontWeight: '700' },
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
input: { borderWidth: 1, borderRadius: 12, padding: 15, height: 100, textAlignVertical: 'top' }
cardCalendar: { borderRadius: 30, padding: 10, borderWidth: 1, overflow: 'hidden' },
card: { padding: 20, borderRadius: 25, marginTop: 20, borderWidth: 1 },
cardTitulo: { fontSize: 18, fontWeight: '700' },
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 },
input: { borderWidth: 1, borderRadius: 15, padding: 15, height: 100, textAlignVertical: 'top' },
btnSalvar: { padding: 15, borderRadius: 15, marginTop: 15, alignItems: 'center' },
btnUpload: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 15, borderRadius: 15, borderWidth: 2, borderStyle: 'dashed' },
justificadoBox: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 10 },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
locationModal: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 30, alignItems: 'center', paddingBottom: 50 },
modalHandle: { width: 50, height: 6, backgroundColor: '#cbd5e1', borderRadius: 10, marginBottom: 30 },
iconCircle: { width: 90, height: 90, borderRadius: 45, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
modalTitle: { fontSize: 24, fontWeight: '800', marginBottom: 12 },
modalDesc: { fontSize: 16, textAlign: 'center', lineHeight: 24, marginBottom: 35, paddingHorizontal: 15 },
btnConfirmar: { width: '100%', padding: 18, borderRadius: 20, alignItems: 'center' },
btnFechar: { marginTop: 20 },
txtFechar: { fontWeight: '700', fontSize: 16 }
});
export default AlunoHome;

View File

@@ -1,11 +1,11 @@
// app/Definicoes.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useMemo, useState } from 'react'; // Importado useMemo e memo
import { memo, useCallback, useMemo, useState } from 'react';
import {
Alert,
Animated,
Linking,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Switch,
@@ -13,149 +13,219 @@ import {
TouchableOpacity,
View
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
const Definicoes = memo(() => {
const router = useRouter();
const [notificacoes, setNotificacoes] = useState(true);
const insets = useSafeAreaInsets();
const { isDarkMode, toggleTheme } = useTheme();
const [notificacoes, setNotificacoes] = useState(true);
// --- SISTEMA DE AVISOS MODERNOS ---
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
const alertOpacity = useMemo(() => new Animated.Value(0), []);
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 azulPetroleo = '#2390a6';
// Otimização de cores para evitar lag no render
const cores = useMemo(() => ({
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#ffffff' : '#000000',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
borda: isDarkMode ? '#333' : '#f1f3f5',
sair: '#dc3545',
azul: '#0d6efd'
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
vermelho: '#EF4444',
vermelhoSuave: 'rgba(239, 68, 68, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
}), [isDarkMode]);
const handleLogout = () => {
Alert.alert(
"Terminar Sessão",
"Tem a certeza que deseja sair da aplicação?",
[
{ text: "Cancelar", style: "cancel" },
{
text: "Sair",
style: "destructive",
onPress: () => router.replace('/')
}
]
);
const handleLogout = async () => {
try {
await supabase.auth.signOut();
router.replace('/');
} catch (e) {
showAlert("Erro ao sair da conta", "error");
}
};
const abrirEmail = () => Linking.openURL(`mailto:epvc@epvc.pt`);
const abrirEmail2 = () => Linking.openURL(`mailto:secretaria@epvc.pt`);
const abrirTelefone = () => Linking.openURL('tel:252 641 805');
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");
});
};
return (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{ width: 40 }} />
</View>
<View style={styles.container}>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.subtituloSecao, { color: cores.azul }]}>Preferências</Text>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name="notifications-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Notificações</Text>
</View>
<Switch
value={notificacoes}
onValueChange={setNotificacoes}
trackColor={{ false: '#767577', true: cores.azul }}
/>
</View>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name={isDarkMode ? "moon" : "sunny-outline"} size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Modo escuro</Text>
</View>
<Switch
value={isDarkMode}
onValueChange={toggleTheme} // Chamada direta otimizada pelo ThemeContext
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={isDarkMode ? '#fff' : '#f4f3f4'}
/>
</View>
<Text style={[styles.subtituloSecao, { color: cores.azul, marginTop: 25 }]}>Suporte e Contactos</Text>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail}>
<View style={styles.iconTexto}>
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Direção</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>epvc@epvc.pt</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail2}>
<View style={styles.iconTexto}>
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Secretaria</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>secretaria@epvc.pt</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirTelefone}>
<View style={styles.iconTexto}>
<Ionicons name="call-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Ligar para a Escola</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>252 641 805</Text>
</TouchableOpacity>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name="information-circle-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Versão da app</Text>
</View>
<Text style={{ color: cores.textoSecundario }}>26.1.10</Text>
</View>
{/* BANNER DE FEEDBACK */}
{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>
)}
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER ESTILIZADO */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.linha, { borderBottomWidth: 0 }]}
onPress={handleLogout}
style={[styles.btnVoltar, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<View style={styles.iconTexto}>
<Ionicons name="log-out-outline" size={22} color={cores.sair} style={{marginRight: 10}} />
<Text style={{ color: cores.sair, fontWeight: '600' }}>Terminar Sessão</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.textoSecundario} />
<Ionicons name="chevron-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{ width: 45 }} />
</View>
</View>
</SafeAreaView>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* GRUPO: PERSONALIZAÇÃO */}
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Personalização</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="notifications" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações Push</Text>
<Switch
value={notificacoes}
onValueChange={(v) => {
setNotificacoes(v);
showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info");
}}
trackColor={{ false: '#CBD5E1', true: cores.azul }}
thumbColor="#FFFFFF"
/>
</View>
<View style={styles.item}>
<View style={[styles.iconContainer, { backgroundColor: isDarkMode ? '#334155' : '#F1F5F9' }]}>
<Ionicons name={isDarkMode ? "moon" : "sunny"} size={20} color={isDarkMode ? '#FACC15' : '#F59E0B'} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Interface Escura</Text>
<Switch
value={isDarkMode}
onValueChange={toggleTheme}
trackColor={{ false: '#CBD5E1', true: cores.azul }}
thumbColor="#FFFFFF"
/>
</View>
</View>
{/* GRUPO: CONTACTOS EPVC */}
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 25 }]}>Escola Profissional</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<SettingLink
icon="business"
label="Direção Geral"
subLabel="epvc@epvc.pt"
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
cores={cores}
showBorder
/>
<SettingLink
icon="mail"
label="Secretaria"
subLabel="secretaria@epvc.pt"
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
cores={cores}
showBorder
/>
<SettingLink
icon="call"
label="Linha Direta"
subLabel="252 641 805"
onPress={() => abrirURL('tel:252641805')}
cores={cores}
/>
</View>
{/* GRUPO: SEGURANÇA & INFO */}
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 25 }]}>
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Versão Estável</Text>
<Text style={[styles.versionBadge, { color: cores.secundario }]}>v2.6.17</Text>
</View>
<TouchableOpacity style={styles.item} onPress={handleLogout}>
<View style={[styles.iconContainer, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '800' }]}>Terminar Sessão</Text>
<Ionicons name="chevron-forward" size={18} color={cores.vermelho} opacity={0.5} />
</TouchableOpacity>
</View>
<View style={styles.footer}>
<Text style={[styles.footerText, { color: cores.secundario }]}>Desenvolvido para PAP 2026</Text>
<Text style={[styles.footerText, { color: cores.azul, fontWeight: '800', marginTop: 4 }]}>EPVC</Text>
</View>
</ScrollView>
</SafeAreaView>
</View>
);
});
// Componente Auxiliar para Links
const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => (
<TouchableOpacity
style={[styles.item, showBorder && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
onPress={onPress}
>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>{label}</Text>
<Text style={{ color: cores.secundario, fontSize: 12, marginTop: 2 }}>{subLabel}</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.3} />
</TouchableOpacity>
);
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
safe: { flex: 1 },
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 8 },
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1, fontSize: 14 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 },
tituloGeral: { fontSize: 24, fontWeight: 'bold' },
subtituloSecao: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase', marginBottom: 5, marginLeft: 5 },
container: { padding: 20 },
card: { paddingHorizontal: 20, paddingVertical: 15, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
linha: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 15, borderBottomWidth: 1 },
iconTexto: { flexDirection: 'row', alignItems: 'center' }
btnVoltar: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
scrollContent: { paddingHorizontal: 20, paddingBottom: 50 },
sectionLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 1.2 },
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 15, elevation: 2 },
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16 },
iconContainer: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700' },
versionBadge: { fontSize: 13, fontWeight: 'bold', backgroundColor: 'rgba(0,0,0,0.05)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
footer: { alignItems: 'center', marginTop: 40, opacity: 0.6 },
footerText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 }
});
export default Definicoes;

View File

@@ -1,25 +1,55 @@
// app/Aluno/PerfilAluno.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator, Alert, Platform, SafeAreaView, ScrollView, StatusBar,
ActivityIndicator, Animated,
ScrollView, StatusBar,
StyleSheet, Text, TextInput, TouchableOpacity, View
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
export default function PerfilAluno() {
const { isDarkMode } = useTheme();
const router = useRouter();
const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [perfil, setPerfil] = useState<any>(null);
const [estagio, setEstagio] = useState<any>(null);
const [contagemPresencas, setContagemPresencas] = useState(0);
const [contagemFaltas, setContagemFaltas] = useState(0);
// --- FUNÇÕES DE FORMATAÇÃO E CÁLCULO ---
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
const alertOpacity = useMemo(() => new Animated.Value(0), []);
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(3000),
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
]).start(() => setAlertConfig(null));
}, []);
const azulPetroleo = '#2390a6';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
vermelho: '#EF4444',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
laranja: '#dd8707',
}), [isDarkMode]);
const formatarParaExibir = (data: string) => {
if (!data) return '';
@@ -41,64 +71,33 @@ export default function PerfilAluno() {
return formatted;
};
// NOVA FUNÇÃO: Calcula horas totais excluindo fins de semana e feriados
const calcularHorasTotaisUteis = (dataInicio: string, dataFim: string) => {
if (!dataInicio || !dataFim) return 400; // Fallback caso a BD esteja vazia
if (!dataInicio || !dataFim) return 400;
let inicio = new Date(dataInicio);
let fim = new Date(dataFim);
let diasUteis = 0;
// Lista de feriados 2026 (Exemplo Portugal)
const feriados = [
'2026-01-01', '2026-04-03', '2026-04-05', '2026-04-25',
'2026-05-01', '2026-06-04', '2026-06-10', '2026-08-15',
'2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25'
];
const feriados = ['2026-01-01', '2026-04-03', '2026-04-25', '2026-05-01', '2026-06-10', '2026-08-15', '2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25'];
let dataAtual = new Date(inicio);
while (dataAtual <= fim) {
const diaSemana = dataAtual.getDay(); // 0: Domingo, 6: Sábado
const diaSemana = dataAtual.getDay();
const dataIso = dataAtual.toISOString().split('T')[0];
if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) {
diasUteis++;
}
if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) diasUteis++;
dataAtual.setDate(dataAtual.getDate() + 1);
}
return diasUteis * 7; // Multiplicado pelas 7h diárias
return diasUteis * 7;
};
const calcularIdade = (dataExibicao: string) => {
if (!dataExibicao || dataExibicao.length < 10) return '--';
try {
const [dia, mes, ano] = dataExibicao.split('-').map(Number);
const hoje = new Date();
const nascimento = new Date(ano, mes - 1, dia);
let idade = hoje.getFullYear() - nascimento.getFullYear();
const m = hoje.getMonth() - nascimento.getMonth();
if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) {
idade--;
}
return isNaN(idade) ? '--' : idade;
} catch {
return '--';
}
};
useEffect(() => {
carregarDados();
}, []);
async function carregarDados() {
const carregarDados = async () => {
try {
setLoading(true);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const [perfilRes, estagioRes, presencasRes] = await Promise.all([
supabase.from('profiles').select('*').eq('id', user.id).single(),
supabase.from('estagios').select('*, empresas(*)').eq('aluno_id', user.id).single(),
supabase.from('presencas').select('estado').eq('aluno_id', user.id)
supabase.from('presencas').select('*').eq('aluno_id', user.id)
]);
const dadosPerfil = perfilRes.data;
@@ -107,23 +106,34 @@ export default function PerfilAluno() {
}
setPerfil({ ...dadosPerfil, email: user.email });
setEstagio(estagioRes.data);
const dadosEstagio = estagioRes.data;
setEstagio(dadosEstagio);
if (presencasRes.data) {
setContagemPresencas(presencasRes.data.filter((p: any) => p.estado === 'presente').length);
setContagemFaltas(presencasRes.data.filter((p: any) => p.estado === 'faltou').length);
if (presencasRes.data && dadosEstagio) {
const dataInicio = new Date(dadosEstagio.data_inicio);
const dataFim = new Date(dadosEstagio.data_fim);
// Filtrar apenas presenças que ocorrem DENTRO do intervalo do estágio
const presencasValidas = presencasRes.data.filter((p: any) => {
const dataP = new Date(p.data);
return dataP >= dataInicio && dataP <= dataFim;
});
setContagemPresencas(presencasValidas.filter((p: any) => p.estado === 'presente').length);
setContagemFaltas(presencasValidas.filter((p: any) => p.estado === 'faltou').length);
}
} catch (e) {
console.error(e);
showAlert('Erro ao carregar dados.', 'error');
} finally {
setLoading(false);
}
}
};
useEffect(() => { carregarDados(); }, []);
const salvarPerfil = async () => {
try {
const dataBD = formatarParaSalvar(perfil.data_nascimento);
const { error } = await supabase.from('profiles').update({
nome: perfil.nome,
telefone: perfil.telefone,
@@ -133,220 +143,207 @@ export default function PerfilAluno() {
if (error) throw error;
setIsEditing(false);
Alert.alert("Sucesso", "Perfil atualizado!");
showAlert('Perfil atualizado!', 'success');
} catch (e) {
Alert.alert("Erro", "Verifica se a data está correta (DD-MM-AAAA).");
showAlert('Verifica se a data está correta.', 'error');
}
};
// CÁLCULOS DO DASHBOARD
const horasPorDia = 7;
const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim);
const horasRealizadas = contagemPresencas * horasPorDia;
const horasRestantes = Math.max(0, horasTotais - horasRealizadas);
const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0;
const themeStyles = {
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: '#3B82F6',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
vermelho: '#EF4444'
const terminarSessao = async () => {
await supabase.auth.signOut();
router.replace('/');
};
if (loading) return <View style={[styles.centered, { backgroundColor: themeStyles.fundo }]}><ActivityIndicator size="large" color={themeStyles.azul} /></View>;
const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim);
const horasRealizadas = contagemPresencas * 7;
const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0;
if (loading) return <View style={[styles.centered, { backgroundColor: cores.fundo }]}><ActivityIndicator size="large" color={cores.azul} /></View>;
return (
<SafeAreaView style={[styles.safe, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={styles.topNav}>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
</TouchableOpacity>
</View>
{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" : "checkmark-circle"} size={20} color="#fff" />
<Text style={styles.alertText}>{alertConfig.msg}</Text>
</Animated.View>
)}
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
<View style={styles.header}>
<View style={[styles.avatarCircle, { backgroundColor: themeStyles.azul }]}>
<Text style={styles.avatarText}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
</View>
<Text style={[styles.userName, { color: themeStyles.texto }]}>{perfil?.nome}</Text>
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
<View style={styles.topBar}>
<TouchableOpacity style={[styles.backBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
<TouchableOpacity
style={[styles.btnEdit, { backgroundColor: isEditing ? '#10B981' : themeStyles.card, borderColor: themeStyles.borda }]}
onPress={isEditing ? salvarPerfil : () => setIsEditing(true)}
style={[styles.editBtn, { backgroundColor: isEditing ? cores.azul : cores.card }]}
onPress={() => isEditing ? salvarPerfil() : setIsEditing(true)}
>
<Ionicons name={isEditing ? "checkmark-outline" : "create-outline"} size={18} color={isEditing ? "#fff" : themeStyles.azul} />
<Text style={[styles.btnEditText, { color: isEditing ? "#fff" : themeStyles.texto }]}>
{isEditing ? "Guardar" : "Editar Perfil"}
</Text>
<Ionicons name={isEditing ? "checkmark" : "create-outline"} size={20} color={isEditing ? "#fff" : cores.azul} />
</TouchableOpacity>
</View>
{/* PROGRESSO */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Progresso do Estágio</Text>
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.statsGrid}>
<StatBox label="Realizadas" valor={`${horasRealizadas}h`} cor={themeStyles.azul} theme={themeStyles} />
<StatBox label="Faltam" valor={`${horasRestantes}h`} cor="#F59E0B" theme={themeStyles} />
<StatBox label="Faltas" valor={contagemFaltas.toString()} cor={themeStyles.vermelho} theme={themeStyles} />
</View>
<View style={styles.progressContainer}>
<View style={[styles.progressBarBase, { backgroundColor: themeStyles.fundo }]}>
<View style={[styles.progressBarFill, { width: `${progresso * 100}%`, backgroundColor: themeStyles.azul }]} />
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<View style={styles.profileHeader}>
<View style={[styles.avatarContainer, { borderColor: cores.azulSuave }]}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
</View>
<Text style={[styles.progressText, { color: themeStyles.secundario }]}>
{Math.round(progresso * 100)}% das {horasTotais}h concluídas (Dias Úteis)
</Text>
</View>
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.userRole, { color: cores.secundario }]}> Aluno: {perfil?.n_escola || '---'}</Text>
</View>
{/* PROGRESS CARD */}
<View style={[styles.card, { backgroundColor: cores.card, marginBottom: 25 }]}>
<Text style={[styles.inputLabel, { color: cores.secundario, marginBottom: 15 }]}>Progresso do Estágio</Text>
<View style={styles.statsGrid}>
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cores.azul }]}>{horasRealizadas}h</Text>
<Text style={[styles.statLabel, { color: cores.secundario }]}>Feitas</Text>
</View>
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cores.laranja }]}>{Math.max(0, horasTotais - horasRealizadas)}h</Text>
<Text style={[styles.statLabel, { color: cores.secundario }]}>Restam</Text>
</View>
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cores.vermelho }]}>{contagemFaltas}</Text>
<Text style={[styles.statLabel, { color: cores.secundario }]}>Faltas</Text>
</View>
</View>
<View style={[styles.progressBarBase, { backgroundColor: cores.fundo, marginTop: 15 }]}>
<View style={[styles.progressBarFill, { width: `${progresso * 100}%`, backgroundColor: cores.azul }]} />
</View>
<Text style={[styles.progressText, { color: cores.secundario }]}>{Math.round(progresso * 100)}% das {horasTotais}h úteis concluídas</Text>
</View>
{/* INFO CARD */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<ModernInput label="Nome Completo" icon="person-outline" value={perfil?.nome || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, nome: v})} cores={cores} />
<ModernInput label="E-mail Institucional" icon="mail-outline" value={perfil?.email || ''} editable={false} cores={cores} />
<View style={styles.row}>
{/* Aumentado o flex da data para 1.2 para evitar corte */}
<View style={{ flex: 1.2, marginRight: 10 }}>
<ModernInput
label="Nascimento"
icon="calendar-outline"
value={perfil?.data_nascimento || ''}
editable={isEditing}
onChangeText={(v: string) => setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})}
cores={cores}
maxLength={10}
keyboardType="numeric"
placeholder="DD-MM-AAAA"
/>
</View>
<View style={{ flex: 1.3 }}>
<ModernInput label="Telefone" icon="call-outline" value={perfil?.telefone || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, telefone: v})} keyboardType="phone-pad" cores={cores} />
</View>
</View>
<ModernInput label="Residência" icon="location-outline" value={perfil?.residencia || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, residencia: v})} cores={cores} />
</View>
{/* ESTÁGIO CARD ATUALIZADO */}
<View style={[styles.card, { backgroundColor: cores.card, marginTop: 20 }]}>
<Text style={[styles.inputLabel, { color: cores.secundario, marginBottom: 15 }]}>Informações do Estágio</Text>
<View style={styles.row}>
<View style={{ flex: 1 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Início</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{formatarParaExibir(estagio?.data_inicio)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Fim</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{formatarParaExibir(estagio?.data_fim)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Horário</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{estagio?.horario || '09:00-17:00'}</Text>
</View>
</View>
<View style={{ marginTop: 15 }}>
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Empresa e Tutor</Text>
<Text style={{ color: cores.texto, fontWeight: '700' }}>{estagio?.empresas?.nome}</Text>
<Text style={{ color: cores.secundario, fontSize: 13 }}>{estagio?.empresas?.tutor_nome}</Text>
</View>
</View>
</View>
{/* DADOS ACADÉMICOS */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Dados de Aluno</Text>
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<InfoRow icon="id-card-outline" label="Nº de Aluno" valor={perfil?.n_escola || '---'} theme={themeStyles} />
<Divider theme={themeStyles} />
<InfoRow icon="mail-outline" label="E-mail Institucional" valor={perfil?.email} theme={themeStyles} />
<View style={[styles.actionsContainer, { marginTop: 25 }]}>
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card }]} onPress={() => router.push('/Aluno/redefenirsenha')}>
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</TouchableOpacity>
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card }]} onPress={terminarSessao}>
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
</View>
{/* INFORMAÇÃO PESSOAL */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Informação Pessoal</Text>
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<EditableRow icon="person-outline" label="Nome Completo" value={perfil?.nome} isEditing={isEditing} onChange={(t: string) => setPerfil({...perfil, nome: t})} theme={themeStyles} />
<Divider theme={themeStyles} />
<EditableRow
icon="calendar-outline"
label="Data de Nascimento"
value={perfil?.data_nascimento}
isEditing={isEditing}
onChange={(t: string) => setPerfil({...perfil, data_nascimento: aplicarMascaraData(t)})}
theme={themeStyles}
keyboard="numeric"
limit={10}
/>
<Divider theme={themeStyles} />
{isEditing && (
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setIsEditing(false); carregarDados(); }}>
<Text style={[styles.cancelText, { color: cores.secundario }]}>Cancelar Edição</Text>
</TouchableOpacity>
)}
<InfoRow
icon="hourglass-outline"
label="Idade Atual"
valor={calcularIdade(perfil?.data_nascimento) !== '--' ? `${calcularIdade(perfil?.data_nascimento)} anos` : '---'}
theme={themeStyles}
/>
<Divider theme={themeStyles} />
<EditableRow icon="call-outline" label="Telemóvel" value={perfil?.telefone} isEditing={isEditing} onChange={(t: string) => setPerfil({...perfil, telefone: t})} theme={themeStyles} keyboard="phone-pad" />
<Divider theme={themeStyles} />
<EditableRow icon="location-outline" label="Residência" value={perfil?.residencia} isEditing={isEditing} onChange={(t: string) => setPerfil({...perfil, residencia: t})} theme={themeStyles} />
</View>
</View>
{/* EMPRESA E DATAS DO ESTÁGIO */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Horário e Local</Text>
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<InfoRow icon="calendar-outline" label="Início do Estágio" valor={formatarParaExibir(estagio?.data_inicio)} theme={themeStyles} />
<Divider theme={themeStyles} />
<InfoRow icon="flag-outline" label="Fim do Estágio" valor={formatarParaExibir(estagio?.data_fim)} theme={themeStyles} />
<Divider theme={themeStyles} />
<InfoRow icon="time-outline" label="Horário Diário" valor={estagio?.horario || "09:00 - 17:00"} theme={themeStyles} />
<Divider theme={themeStyles} />
<InfoRow icon="business-outline" label="Empresa" valor={estagio?.empresas?.nome} theme={themeStyles} />
<Divider theme={themeStyles} />
<InfoRow icon="person-outline" label="Tutor" valor={estagio?.empresas?.tutor_nome} theme={themeStyles} />
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
// --- AUXILIARES (IGUAIS) ---
function StatBox({ label, valor, cor, theme }: any) {
return (
<View style={styles.statBox}>
<Text style={[styles.statValor, { color: cor }]}>{valor}</Text>
<Text style={[styles.statLabel, { color: theme.secundario }]}>{label}</Text>
</ScrollView>
</SafeAreaView>
</View>
);
}
function EditableRow({ icon, label, value, isEditing, onChange, theme, keyboard = "default", limit }: any) {
return (
<View style={styles.infoRow}>
<Ionicons name={icon} size={20} color={theme.azul} style={{ width: 30 }} />
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: theme.secundario }]}>{label}</Text>
{isEditing ? (
<TextInput
style={[styles.input, { color: theme.texto, borderBottomColor: theme.azul }]}
value={value}
onChangeText={onChange}
keyboardType={keyboard}
maxLength={limit}
placeholder={label.includes("Data") ? "DD-MM-AAAA" : ""}
placeholderTextColor={theme.secundario}
/>
) : (
<Text style={[styles.infoValor, { color: theme.texto }]}>{value || '---'}</Text>
)}
</View>
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: cores.fundo, borderColor: editable ? cores.azul : cores.borda, opacity: editable ? 1 : 0.8 }]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 8 }} />
<TextInput {...props} editable={editable} style={[styles.textInput, { color: cores.texto }]} placeholderTextColor={cores.secundario} />
</View>
);
}
function InfoRow({ icon, label, valor, theme }: any) {
return (
<View style={styles.infoRow}>
<Ionicons name={icon} size={20} color={theme.azul} style={{ width: 30 }} />
<View style={styles.infoContent}>
<Text style={[styles.infoLabel, { color: theme.secundario }]}>{label}</Text>
<Text style={[styles.infoValor, { color: theme.texto }]}>{valor || '---'}</Text>
</View>
</View>
);
}
const Divider = ({ theme }: any) => <View style={[styles.divider, { backgroundColor: theme.borda }]} />;
</View>
);
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
topNav: { paddingHorizontal: 20, paddingTop: 10, height: 50, justifyContent: 'center' },
backBtn: { width: 40, height: 40, justifyContent: 'center' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
safe: { flex: 1 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
header: { alignItems: 'center', marginBottom: 30 },
avatarCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
avatarText: { fontSize: 32, fontWeight: 'bold', color: '#fff' },
userName: { fontSize: 20, fontWeight: '800', marginBottom: 15 },
btnEdit: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, borderWidth: 1 },
btnEditText: { marginLeft: 8, fontSize: 13, fontWeight: '600' },
section: { marginBottom: 25 },
sectionTitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10, marginLeft: 5 },
card: { borderRadius: 20, padding: 16, borderWidth: 1 },
statsGrid: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 20 },
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 15, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 10 },
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 },
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 },
topTitle: { fontSize: 18, fontWeight: '800' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
profileHeader: { alignItems: 'center', marginVertical: 20 },
avatarContainer: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' },
avatar: { width: 70, height: 70, borderRadius: 35, alignItems: 'center', justifyContent: 'center', elevation: 4 },
avatarLetter: { color: '#fff', fontSize: 28, fontWeight: '800' },
userName: { fontSize: 20, fontWeight: '800', marginTop: 12 },
userRole: { fontSize: 13, fontWeight: '500' },
card: { borderRadius: 24, padding: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 },
statsGrid: { flexDirection: 'row', justifyContent: 'space-around' },
statBox: { alignItems: 'center' },
statValor: { fontSize: 18, fontWeight: 'bold' },
statLabel: { fontSize: 11, fontWeight: '600', marginTop: 2 },
progressContainer: { marginTop: 10 },
statValor: { fontSize: 18, fontWeight: '800' },
statLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
progressBarBase: { height: 8, borderRadius: 4, overflow: 'hidden' },
progressBarFill: { height: '100%', borderRadius: 4 },
progressText: { fontSize: 11, textAlign: 'center', marginTop: 8, fontWeight: '600' },
infoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8 },
infoContent: { flex: 1 },
infoLabel: { fontSize: 10, fontWeight: '600', textTransform: 'uppercase' },
infoValor: { fontSize: 15, fontWeight: '600', marginTop: 2 },
input: { fontSize: 15, fontWeight: '600', paddingVertical: 2, borderBottomWidth: 1 },
divider: { height: 1, width: '100%', marginVertical: 8 }
progressText: { fontSize: 10, textAlign: 'center', marginTop: 8, fontWeight: '700' },
inputWrapper: { marginBottom: 15 },
inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 },
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, height: 50, borderRadius: 16, borderWidth: 1.5 },
textInput: { flex: 1, fontSize: 14, fontWeight: '600' },
row: { flexDirection: 'row' },
actionsContainer: { gap: 10 },
menuItem: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 18, elevation: 1 },
menuIcon: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
menuText: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' },
cancelBtn: { marginTop: 20, alignItems: 'center' },
cancelText: { fontSize: 13, fontWeight: '600', textDecorationLine: 'underline' }
});

View File

@@ -1,3 +1,4 @@
// app/(Professor)/HistoricoPresencas.tsx
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
@@ -40,15 +41,16 @@ export default function HistoricoPresencas() {
const nomeStr = Array.isArray(params.nome) ? params.nome[0] : params.nome;
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: '#3B82F6',
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
vermelho: '#EF4444',
fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9',
card: isDarkMode ? '#161618' : '#FFFFFF',
texto: isDarkMode ? '#FFFFFF' : '#1A1C1E',
secundario: isDarkMode ? '#8E8E93' : '#6C757D',
azul: '#2390a6',
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
vermelho: '#FF453A',
verde: '#32D74B',
borda: isDarkMode ? '#2C2C2E' : '#E9ECEF',
linha: isDarkMode ? '#2C2C2E' : '#D1D9E6',
}), [isDarkMode]);
useEffect(() => {
@@ -58,16 +60,41 @@ export default function HistoricoPresencas() {
async function fetchHistorico() {
try {
setLoading(true);
const hoje = new Date().toISOString().split('T')[0];
// 1. Procurar o estágio ativo (mesma lógica das Faltas)
const { data: listaEstagios, error: errEstagio } = await supabase
.from('estagios')
.select('data_inicio, data_fim')
.lte('data_inicio', hoje)
.gte('data_fim', hoje)
.limit(1);
if (errEstagio) throw errEstagio;
// Fallback caso não haja estágio hoje (Janeiro a Dezembro de 2026)
let inicio = '2026-01-01';
let fim = '2026-12-31';
if (listaEstagios && listaEstagios.length > 0) {
inicio = listaEstagios[0].data_inicio;
fim = listaEstagios[0].data_fim;
}
// 2. Buscar histórico filtrado por esse período
const { data, error } = await supabase
.from('presencas')
.select('*')
.eq('aluno_id', idStr)
.gte('data', inicio)
.lte('data', fim)
.order('data', { ascending: false });
if (error) throw error;
setPresencas(data || []);
} catch (error: any) {
Alert.alert("Erro", "Não foi possível carregar as presenças.");
console.error(error);
Alert.alert("Erro", "Não foi possível carregar as presenças do estágio atual.");
} finally {
setLoading(false);
}
@@ -97,9 +124,7 @@ export default function HistoricoPresencas() {
});
if (url) {
Linking.openURL(url).catch(() => {
Alert.alert("Erro", "Não foi possível abrir o mapa.");
});
Linking.openURL(url).catch(() => Alert.alert("Erro", "Não foi possível abrir o mapa."));
}
};
@@ -107,26 +132,25 @@ export default function HistoricoPresencas() {
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
{/* HEADER PREMIUM */}
<View style={styles.headerFixed}>
<View style={styles.topBar}>
<View style={styles.headerArea}>
<View style={styles.topRow}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<Ionicons name="chevron-back" size={22} color={cores.texto} />
<Ionicons name="arrow-back" size={20} color={cores.texto} />
</TouchableOpacity>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: cores.texto }]}>Histórico</Text>
<Text style={[styles.subtitle, { color: cores.secundario }]} numberOfLines={1}>{nomeStr}</Text>
<View style={styles.titleWrapper}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Histórico</Text>
<Text style={[styles.headerSubtitle, { color: cores.azul }]} numberOfLines={1}>{nomeStr}</Text>
</View>
<TouchableOpacity
onPress={fetchHistorico}
style={[styles.refreshBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<Ionicons name="refresh" size={20} color={cores.azul} />
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
</View>
</View>
@@ -138,63 +162,65 @@ export default function HistoricoPresencas() {
) : (
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{presencas.length === 0 ? (
<View style={styles.empty}>
<Ionicons name="calendar-outline" size={48} color={cores.secundario} style={{ opacity: 0.5 }} />
<Text style={[styles.emptyText, { color: cores.secundario }]}>Sem registos para este aluno.</Text>
<View style={styles.emptyContainer}>
<View style={[styles.emptyIconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="calendar-clear-outline" size={40} color={cores.azul} />
</View>
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem registos neste estágio</Text>
<Text style={[styles.emptySub, { color: cores.secundario }]}>Não foram encontradas presenças ou faltas para o período selecionado.</Text>
</View>
) : (
presencas.map((item) => {
const isPresente = item.estado === 'presente';
return (
<TouchableOpacity
key={item.id}
activeOpacity={0.7}
onPress={() => handleNavigation(item)}
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<View style={styles.cardHeader}>
<View style={[styles.iconBox, { backgroundColor: isPresente ? cores.azulSuave : 'rgba(239, 68, 68, 0.1)' }]}>
<Ionicons
name={isPresente ? "book-outline" : "alert-circle-outline"}
size={20}
color={isPresente ? cores.azul : cores.vermelho}
/>
</View>
<View style={styles.info}>
<Text style={[styles.dateText, { color: cores.texto }]}>
{new Date(item.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })}
</Text>
<View style={styles.statusBadge}>
<View style={[styles.dot, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
<Text style={[styles.subText, { color: cores.secundario }]}>
{item.estado ? item.estado.toUpperCase() : '---'}
</Text>
<View style={styles.timelineWrapper}>
<View style={[styles.timelineLine, { backgroundColor: cores.linha }]} />
{presencas.map((item) => {
const isPresente = item.estado === 'presente';
const dataObj = new Date(item.data);
return (
<View key={item.id} style={styles.timelineItem}>
<View style={[styles.timelineDot, { backgroundColor: isPresente ? cores.verde : cores.vermelho, borderColor: cores.fundo }]} />
<TouchableOpacity
activeOpacity={0.8}
onPress={() => handleNavigation(item)}
style={[styles.modernCard, { backgroundColor: cores.card }]}
>
<View style={styles.cardMain}>
<View style={styles.dateInfo}>
<Text style={[styles.dayText, { color: cores.texto }]}>
{dataObj.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
</Text>
<Text style={[styles.yearText, { color: cores.secundario }]}>
{dataObj.getFullYear()}
</Text>
</View>
<View style={styles.dividerVertical} />
<View style={styles.contentInfo}>
<View style={styles.statusRow}>
<Text style={[styles.statusText, { color: isPresente ? cores.azul : cores.vermelho }]}>
{isPresente ? 'PRESENÇA MARCADA' : 'FALTA REGISTADA'}
</Text>
{item.lat && item.lng && (
<TouchableOpacity onPress={() => abrirMapa(item.lat!, item.lng!)} style={styles.mapSmallBtn}>
<Ionicons name="location" size={14} color={cores.azul} />
</TouchableOpacity>
)}
</View>
<Text style={[styles.infoHint, { color: cores.secundario }]}>
Clique para ver {isPresente ? 'o sumário' : 'a justificação'}
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</View>
</View>
<View style={styles.actionRow}>
{item.lat && item.lng && (
<TouchableOpacity
onPress={() => abrirMapa(item.lat!, item.lng!)}
style={styles.locationBtn}
>
<Ionicons name="location-sharp" size={20} color={cores.azul} />
</TouchableOpacity>
)}
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</View>
</TouchableOpacity>
</View>
<View style={[styles.footerCard, { borderTopColor: isDarkMode ? '#2D2D2D' : '#F1F5F9' }]}>
<Text style={[styles.tapInfo, { color: isPresente ? cores.azul : cores.vermelho }]}>
Ver {isPresente ? 'sumário detalhado' : 'comprovativo de falta'}
</Text>
</View>
</TouchableOpacity>
);
})
);
})}
</View>
)}
</ScrollView>
)}
@@ -204,40 +230,31 @@ export default function HistoricoPresencas() {
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 },
headerFixed: { paddingHorizontal: 20, paddingBottom: 10 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 75 },
backBtnPremium: {
width: 42, height: 42, borderRadius: 14,
justifyContent: 'center', alignItems: 'center',
borderWidth: 1, elevation: 2, shadowColor: '#000',
shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2
},
refreshBtnPremium: {
width: 42, height: 42, borderRadius: 14,
justifyContent: 'center', alignItems: 'center',
borderWidth: 1
},
titleContainer: { flex: 1, alignItems: 'center', paddingHorizontal: 10 },
title: { fontSize: 20, fontWeight: '800' },
subtitle: { fontSize: 12, fontWeight: '600', marginTop: -2 },
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
card: {
borderRadius: 22, padding: 16, marginBottom: 15,
borderWidth: 1, elevation: 3, shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10
},
cardHeader: { flexDirection: 'row', alignItems: 'center' },
iconBox: { width: 44, height: 44, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
info: { flex: 1, marginLeft: 15 },
dateText: { fontSize: 15, fontWeight: '700' },
statusBadge: { flexDirection: 'row', alignItems: 'center', marginTop: 4 },
dot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 },
subText: { fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
actionRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
locationBtn: { width: 34, height: 34, justifyContent: 'center', alignItems: 'center' },
footerCard: { marginTop: 12, paddingTop: 10, borderTopWidth: 1 },
tapInfo: { fontSize: 10, fontWeight: '800', textAlign: 'right', textTransform: 'uppercase', letterSpacing: 0.5 },
headerArea: { paddingHorizontal: 24, paddingBottom: 15, paddingTop: 10 },
topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 60 },
btnAction: { width: 45, height: 45, borderRadius: 16, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
titleWrapper: { alignItems: 'center', flex: 1 },
headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 },
scrollContent: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 50 },
timelineWrapper: { paddingLeft: 10 },
timelineLine: { position: 'absolute', left: 10, top: 0, bottom: 0, width: 2, borderRadius: 1 },
timelineItem: { marginBottom: 20, paddingLeft: 25, justifyContent: 'center' },
timelineDot: { position: 'absolute', left: 6, top: '50%', marginTop: -5, width: 10, height: 10, borderRadius: 5, borderWidth: 2, zIndex: 1 },
modernCard: { borderRadius: 20, padding: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
cardMain: { flexDirection: 'row', alignItems: 'center' },
dateInfo: { alignItems: 'center', minWidth: 50 },
dayText: { fontSize: 15, fontWeight: '800', textTransform: 'uppercase' },
yearText: { fontSize: 11, fontWeight: '600' },
dividerVertical: { width: 1, height: 30, backgroundColor: 'rgba(128,128,128,0.2)', marginHorizontal: 15 },
contentInfo: { flex: 1 },
statusRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 2 },
statusText: { fontSize: 11, fontWeight: '900', letterSpacing: 0.5 },
mapSmallBtn: { marginLeft: 8 },
infoHint: { fontSize: 12, fontWeight: '500' },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
empty: { alignItems: 'center', marginTop: 60 },
emptyText: { marginTop: 15, fontSize: 14, fontWeight: '600', textAlign: 'center' },
emptyContainer: { alignItems: 'center', marginTop: 100 },
emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
emptyText: { fontSize: 18, fontWeight: '800', marginBottom: 8 },
emptySub: { fontSize: 14, textAlign: 'center', paddingHorizontal: 40, lineHeight: 20 },
});

View File

@@ -4,55 +4,41 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Linking,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
View
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
interface AlunoEstado {
id: string;
nome: string;
n_escola: string;
turma_curso: string;
email: string;
telefone: string;
residencia: string;
idade: string;
empresa_nome: string;
tutor_nome: string;
tutor_tel: string;
data_inicio: string;
data_fim: string;
horas_diarias: string;
horarios_detalhados: string[];
}
const DetalhesAlunos = memo(() => {
const router = useRouter();
const params = useLocalSearchParams();
const { isDarkMode } = useTheme();
const insets = useSafeAreaInsets();
const azulPetroleo = '#2390a6';
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
}), [isDarkMode]);
const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null;
const [aluno, setAluno] = useState<AlunoEstado | null>(null);
const [aluno, setAluno] = useState<any>(null);
const [loading, setLoading] = useState(true);
const fetchAluno = async () => {
@@ -72,190 +58,196 @@ const DetalhesAlunos = memo(() => {
.single();
if (error) throw error;
if (data) {
const d = data as any;
let listaHorarios: string[] = [];
if (data?.estagios?.[0]?.id) {
const { data: hData } = await supabase
.from('horarios_estagio')
.select('hora_inicio, hora_fim')
.eq('estagio_id', data.estagios[0].id);
// Garante que pegamos o objeto correto mesmo que venha em Array
const perfil = Array.isArray(d.profiles) ? d.profiles[0] : d.profiles;
const estagio = Array.isArray(d.estagios) ? d.estagios[0] : d.estagios;
const empresa = estagio && (Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas);
let listaHorarios: string[] = [];
if (estagio?.id) {
const { data: hData, error: hError } = await supabase
.from('horarios_estagio')
.select('hora_inicio, hora_fim')
.eq('estagio_id', estagio.id);
if (!hError && hData) {
listaHorarios = hData.map(h => {
// Limpa os segundos caso venham da base de dados (ex: 08:00:00 -> 08:00)
const inicio = h.hora_inicio?.slice(0, 5) || '00:00';
const fim = h.hora_fim?.slice(0, 5) || '00:00';
return `${inicio} às ${fim}`;
});
}
if (hData) {
listaHorarios = hData.map(h => `${h.hora_inicio?.slice(0, 5)} - ${h.hora_fim?.slice(0, 5)}`);
}
setAluno({
id: String(d.id),
nome: d.nome || 'Sem nome',
n_escola: String(d.n_escola || '-'),
turma_curso: d.turma_curso || '-',
email: perfil?.email ?? '-',
telefone: perfil?.telefone ?? '-',
residencia: perfil?.residencia ?? '-',
idade: perfil?.idade ? String(perfil.idade) : '-',
empresa_nome: empresa?.nome || 'Não atribuída',
tutor_nome: empresa?.tutor_nome || 'Não definido',
tutor_tel: empresa?.tutor_telefone || '-',
data_inicio: estagio?.data_inicio || '-',
data_fim: estagio?.data_fim || '-',
horas_diarias: estagio?.horas_diarias ? `${estagio.horas_diarias}h` : '0h',
horarios_detalhados: listaHorarios,
});
}
setAluno({
...data,
perfil: Array.isArray(data.profiles) ? data.profiles[0] : data.profiles,
estagio: Array.isArray(data.estagios) ? data.estagios[0] : data.estagios,
horarios: listaHorarios
});
} catch (err: any) {
console.log('Erro ao carregar aluno:', err.message);
console.log('Erro:', err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (alunoId) fetchAluno();
}, [alunoId]);
useEffect(() => { if (alunoId) fetchAluno(); }, [alunoId]);
if (loading) {
return (
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
);
}
if (loading) return (
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
);
return (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={styles.safe} edges={['top']}>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
{/* HEADER */}
{/* HEADER CLEAN */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
<TouchableOpacity onPress={() => router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}>
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
<TouchableOpacity onPress={fetchAluno} style={[styles.btnAction, { borderColor: cores.borda }]}>
<Ionicons name="refresh-outline" size={20} color={cores.azul} />
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha Aluno</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
Detalhes acadêmicos e estágio
</Text>
</View>
</View>
<ScrollView
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
showsVerticalScrollIndicator={false}
>
{/* SECÇÃO PERFIL */}
<View style={[styles.profileCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.avatarCircle, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarTxt, { color: cores.azul }]}>{aluno?.nome.charAt(0).toUpperCase()}</Text>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 30 }]} showsVerticalScrollIndicator={false}>
{/* PERFIL MINIMALISTA */}
<View style={styles.profileSection}>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarTxt, { color: cores.azul }]}>{aluno?.nome?.charAt(0)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno?.nome}</Text>
<Text style={[styles.alunoCurso, { color: cores.secundario }]}>{aluno?.turma_curso}</Text>
<Text style={[styles.alunoCurso, { color: cores.laranja }]}>{aluno?.turma_curso}</Text>
</View>
</View>
{/* DADOS PESSOAIS */}
{/* DADOS PESSOAIS CARD */}
<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="mail-outline" label="EmailInstitucional" value={aluno?.perfil?.email} cores={cores}
onPress={() => Linking.openURL(`mailto:${aluno?.perfil?.email}`)} />
<DetailRow icon="call-outline" label="Telemóvel" value={aluno?.perfil?.telefone} cores={cores}
onPress={() => Linking.openURL(`tel:${aluno?.perfil?.telefone}`)} />
<DetailRow icon="location-outline" label="Residência" value={aluno?.perfil?.residencia} cores={cores} ultimo />
</View>
{/* SECÇÃO ESTÁGIO */}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Pessoais</Text>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Plano de Estágio</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<DetailRow icon="school" label="Nº Escola" value={aluno?.n_escola} cores={cores} />
<DetailRow icon="mail" label="Email" value={aluno?.email} cores={cores} />
<DetailRow icon="call" label="Contacto" value={aluno?.telefone} cores={cores} />
<DetailRow icon="location" label="Morada" value={aluno?.residencia} cores={cores} ultimo />
</View>
{aluno?.estagio ? (
<View style={[styles.estagioCard, { backgroundColor: cores.azul }]}>
<View style={styles.estagioHeader}>
<Ionicons name="business" size={28} color="rgba(255,255,255,0.4)" />
<View style={styles.statusBadge}><Text style={styles.statusText}>ATIVO</Text></View>
</View>
<Text style={styles.empresaNome}>{aluno?.estagio?.empresas?.nome || 'Empresa Indefinida'}</Text>
<View style={styles.tutorInfo}>
<Text style={styles.miniLabel}>TUTOR NA EMPRESA</Text>
<Text style={styles.tutorNome}>{aluno?.estagio?.empresas?.tutor_nome || 'Não definido'}</Text>
<Text style={styles.tutorTel}>{aluno?.estagio?.empresas?.tutor_telefone || '-'}</Text>
</View>
{/* INFORMAÇÃO DE ESTÁGIO */}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Estágio Atual</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
<View style={styles.horarioBox}>
<Text style={styles.miniLabel}>HORÁRIOS</Text>
{aluno?.horarios && aluno.horarios.length > 0 ? (
aluno.horarios.map((h: string, i: number) => (
<Text key={i} style={styles.horarioTxt}>{h}</Text>
))
) : (
<Text style={styles.horarioTxt}>Não registado</Text>
)}
</View>
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<DetailRow icon="business" label="Empresa" value={aluno?.empresa_nome} cores={cores} />
<DetailRow icon="person-circle" label="Tutor" value={aluno?.tutor_nome} cores={cores} />
<View style={styles.row}>
<Ionicons name="time" size={18} color={cores.azul} style={styles.rowIcon} />
<View style={{ flex: 1 }}>
<Text style={[styles.rowLabel, { color: cores.secundario }]}>Horários Registados</Text>
{aluno?.horarios_detalhados && aluno.horarios_detalhados.length > 0 ? (
aluno.horarios_detalhados.map((h, i) => (
<Text key={i} style={[styles.rowValue, { color: cores.texto }]}>{h}</Text>
))
) : (
<Text style={[styles.rowValue, { color: cores.texto }]}>Não definidos</Text>
)}
</View>
<View style={styles.estagioDivider} />
{/* PERÍODO CORRIGIDO: DUAS COLUNAS COM ÍCONE */}
<View style={styles.estagioFooter}>
<View style={styles.periodoCol}>
<Ionicons name="calendar-outline" size={16} color="rgba(255,255,255,0.6)" style={{marginRight: 8}} />
<View>
<Text style={styles.miniLabel}>INÍCIO</Text>
<Text style={styles.footerVal}>{aluno?.estagio?.data_inicio}</Text>
</View>
</View>
<View style={[styles.periodoCol, {alignItems: 'flex-end'}]}>
<View style={{marginRight: 8, alignItems: 'flex-end'}}>
<Text style={styles.miniLabel}>FIM PREVISTO</Text>
<Text style={styles.footerVal}>{aluno?.estagio?.data_fim}</Text>
</View>
<Ionicons name="calendar" size={16} color="rgba(255,255,255,0.6)" />
</View>
</View>
</View>
<View style={styles.datesRow}>
<View style={{ flex: 1 }}>
<DetailRow icon="calendar" label="Início" value={aluno?.data_inicio} cores={cores} ultimo />
</View>
<View style={{ flex: 1 }}>
<DetailRow icon="calendar-outline" label="Fim" value={aluno?.data_fim} cores={cores} ultimo />
</View>
) : (
<View style={[styles.noEstagio, { borderColor: cores.borda, backgroundColor: cores.card }]}>
<Ionicons name="alert-circle-outline" size={24} color={cores.laranja} />
<Text style={[styles.noEstagioTxt, { color: cores.secundario }]}>Sem estágio atribuído</Text>
</View>
</View>
)}
</ScrollView>
</SafeAreaView>
</View>
);
});
const DetailRow = ({ icon, label, value, cores, ultimo }: any) => (
<View style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<Ionicons name={icon} size={18} color={cores.azul} style={styles.rowIcon} />
const DetailRow = ({ icon, label, value, cores, ultimo, onPress }: any) => (
<TouchableOpacity
activeOpacity={onPress ? 0.6 : 1}
onPress={onPress}
style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda + '50' }]}
>
<Ionicons name={icon} size={20} color={cores.azul} style={{ marginRight: 15 }} />
<View style={{ flex: 1 }}>
<Text style={[styles.rowLabel, { color: cores.secundario }]}>{label}</Text>
<Text style={[styles.rowValue, { color: cores.texto }]}>{value}</Text>
<Text style={[styles.rowValue, { color: cores.texto }]}>{value || '-'}</Text>
</View>
</View>
{onPress && <Ionicons name="chevron-forward" size={14} color={cores.borda} />}
</TouchableOpacity>
);
const styles = StyleSheet.create({
safe: { flex: 1 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 13, fontWeight: '600' },
listPadding: { paddingHorizontal: 20, paddingTop: 10 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 15 },
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 },
sectionLine: { flex: 1, height: 1, borderRadius: 1 },
profileCard: { flexDirection: 'row', alignItems: 'center', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 10 },
avatarCircle: { width: 60, height: 60, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
avatarTxt: { fontSize: 24, fontWeight: '900' },
alunoNome: { fontSize: 18, fontWeight: '800' },
alunoCurso: { fontSize: 14, fontWeight: '600', marginTop: 2 },
infoCard: { borderRadius: 24, borderWidth: 1, paddingHorizontal: 15, paddingVertical: 5 },
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', textTransform: 'uppercase' },
infoCard: { borderRadius: 25, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 5, marginBottom: 30 },
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 15 },
rowIcon: { marginRight: 15, width: 20 },
rowLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 },
rowValue: { fontSize: 15, fontWeight: '700' },
datesRow: { flexDirection: 'row' }
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginRight: 15 },
sectionLine: { flex: 1, height: 1, opacity: 0.3 },
estagioCard: { padding: 25, borderRadius: 32, elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 },
estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
statusText: { color: '#fff', fontSize: 10, fontWeight: '900' },
empresaNome: { color: '#fff', fontSize: 24, fontWeight: '900', marginBottom: 20 },
tutorInfo: { marginBottom: 15 },
miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, fontWeight: '900', marginBottom: 4 },
tutorNome: { color: '#fff', fontSize: 16, fontWeight: '800' },
tutorTel: { color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: '600' },
horarioBox: { marginBottom: 20, backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15 },
horarioTxt: { color: '#fff', fontSize: 14, fontWeight: '700' },
estagioDivider: { height: 1, backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 20 },
// ESTILOS DO RODAPÉ CORRIGIDOS
estagioFooter: { flexDirection: 'row', justifyContent: 'space-between' },
periodoCol: { flex: 1, flexDirection: 'row', alignItems: 'center' },
footerVal: { color: '#fff', fontSize: 14, fontWeight: '800' },
noEstagio: { padding: 30, borderRadius: 25, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center', gap: 10 },
noEstagioTxt: { fontWeight: '700', fontSize: 14 }
});
export default DetalhesAlunos;

View File

@@ -1,7 +1,9 @@
// app/Professor/Estagios/index.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Modal,
ScrollView,
@@ -34,32 +36,31 @@ export default function Estagios() {
const router = useRouter();
const { isDarkMode } = useTheme();
const insets = useSafeAreaInsets();
const azulPetroleo = '#2390a6';
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
vermelho: '#EF4444',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.5)',
overlay: 'rgba(26, 54, 93, 0.8)',
}), [isDarkMode]);
// Estados de Dados
// Estados
const [estagios, setEstagios] = useState<Estagio[]>([]);
const [alunos, setAlunos] = useState<Aluno[]>([]);
const [empresas, setEmpresas] = useState<Empresa[]>([]);
const [loading, setLoading] = useState(true);
// Estados de Modais
const [modalVisible, setModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [estagioParaApagar, setEstagioParaApagar] = useState<{id: string, nome: string} | null>(null);
// Estados de Formulário
const [passo, setPasso] = useState(1);
const [alunoSelecionado, setAlunoSelecionado] = useState<Aluno | null>(null);
const [empresaSelecionada, setEmpresaSelecionada] = useState<Empresa | null>(null);
@@ -68,13 +69,11 @@ export default function Estagios() {
const [dataFim, setDataFim] = useState('');
const [searchMain, setSearchMain] = useState('');
// Horários
const [hManhaIni, setHManhaIni] = useState('');
const [hManhaFim, setHManhaFim] = useState('');
const [hTardeIni, setHTardeIni] = useState('');
const [hTardeFim, setHTardeFim] = useState('');
// --- Lógica de Dados ---
useEffect(() => { fetchDados(); }, []);
const fetchDados = async () => {
@@ -91,9 +90,25 @@ export default function Estagios() {
} catch (e) { console.error(e); } finally { setLoading(false); }
};
const carregarHorariosEdicao = async (estagioId: string) => {
const { data } = await supabase.from('horarios_estagio').select('*').eq('estagio_id', estagioId);
if (data) {
setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
data.forEach(h => {
if (h.periodo === 'Manhã') {
setHManhaIni(h.hora_inicio?.slice(0, 5) || '');
setHManhaFim(h.hora_fim?.slice(0, 5) || '');
} else if (h.periodo === 'Tarde') {
setHTardeIni(h.hora_inicio?.slice(0, 5) || '');
setHTardeFim(h.hora_fim?.slice(0, 5) || '');
}
});
}
};
const totalHorasDiarias = useMemo(() => {
const calcularMinutos = (ini: string, fim: string) => {
if (!ini || !fim) return 0;
if (!ini || !fim || !ini.includes(':') || !fim.includes(':')) return 0;
const [hI, mI] = ini.split(':').map(Number);
const [hF, mF] = fim.split(':').map(Number);
const totalI = (hI || 0) * 60 + (mI || 0);
@@ -106,197 +121,207 @@ export default function Estagios() {
return m > 0 ? `${h}h${m}m` : `${h}h`;
}, [hManhaIni, hManhaFim, hTardeIni, hTardeFim]);
// --- Funções de Ação ---
const prepararApagar = (id: string, nome: string) => {
setEstagioParaApagar({ id, nome });
setDeleteModalVisible(true);
const formatarHoraPostgres = (time: string) => {
if (!time || !time.includes(':')) return null;
const [h, m] = time.split(':');
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`;
};
const salvarEstagio = async () => {
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (empresaSelecionada) {
await supabase.from('empresas').update({
tutor_nome: empresaSelecionada.tutor_nome,
tutor_telefone: empresaSelecionada.tutor_telefone
}).eq('id', empresaSelecionada.id);
}
const payload = {
aluno_id: alunoSelecionado?.id,
empresa_id: empresaSelecionada?.id,
professor_id: user?.id,
data_inicio: dataInicio || new Date().toISOString().split('T')[0],
data_fim: dataFim || null,
horas_diarias: totalHorasDiarias,
estado: 'Ativo',
};
const { data: estData, error: errE } = editandoEstagio
? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single()
: await supabase.from('estagios').insert([payload]).select().single();
if (errE) throw errE;
const currentId = editandoEstagio?.id || estData.id;
await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId);
const novosHorarios = [];
if (hManhaIni && hManhaFim) {
novosHorarios.push({ estagio_id: currentId, periodo: 'Manhã', hora_inicio: formatarHoraPostgres(hManhaIni), hora_fim: formatarHoraPostgres(hManhaFim) });
}
if (hTardeIni && hTardeFim) {
novosHorarios.push({ estagio_id: currentId, periodo: 'Tarde', hora_inicio: formatarHoraPostgres(hTardeIni), hora_fim: formatarHoraPostgres(hTardeFim) });
}
if (novosHorarios.length > 0) await supabase.from('horarios_estagio').insert(novosHorarios);
handleFecharModal();
fetchDados();
} catch (error: any) { Alert.alert("Erro", error.message); } finally { setLoading(false); }
};
const confirmarEliminacao = async () => {
if (!estagioParaApagar) return;
setLoading(true);
const { error } = await supabase.from('estagios').delete().eq('id', estagioParaApagar.id);
if (error) Alert.alert("Erro", "Não foi possível apagar.");
else {
setDeleteModalVisible(false);
setEstagioParaApagar(null);
fetchDados();
}
if (!error) { setDeleteModalVisible(false); fetchDados(); }
setLoading(false);
};
const salvarEstagio = async () => {
setLoading(true);
const { data: { user } } = await supabase.auth.getUser();
if (empresaSelecionada) {
await supabase.from('empresas').update({
tutor_nome: empresaSelecionada.tutor_nome,
tutor_telefone: empresaSelecionada.tutor_telefone
}).eq('id', empresaSelecionada.id);
}
const payload = {
aluno_id: alunoSelecionado?.id,
empresa_id: empresaSelecionada?.id,
professor_id: user?.id,
data_inicio: dataInicio || new Date().toISOString().split('T')[0],
data_fim: dataFim || null,
horas_diarias: totalHorasDiarias,
estado: 'Ativo',
};
const { data: estData, error: errE } = editandoEstagio
? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single()
: await supabase.from('estagios').insert([payload]).select().single();
if (errE) { setLoading(false); return Alert.alert("Erro", errE.message); }
const currentId = editandoEstagio?.id || estData.id;
await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId);
const horarios = [];
if (hManhaIni && hManhaFim) horarios.push({ estagio_id: currentId, periodo: 'Manhã', hora_inicio: hManhaIni, hora_fim: hManhaFim });
if (hTardeIni && hTardeFim) horarios.push({ estagio_id: currentId, periodo: 'Tarde', hora_inicio: hTardeIni, hora_fim: hTardeFim });
if (horarios.length > 0) await supabase.from('horarios_estagio').insert(horarios);
handleFecharModal();
fetchDados();
};
const handleFecharModal = () => {
setModalVisible(false);
setEditandoEstagio(null);
setAlunoSelecionado(null);
setEmpresaSelecionada(null);
setDataInicio(''); setDataFim('');
setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
setPasso(1);
};
// --- Filtros ---
const estagiosFiltrados = useMemo(() => {
const groups: Record<string, Estagio[]> = {};
estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => {
const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Sem Turma';
const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Geral';
if (!groups[chave]) groups[chave] = [];
groups[chave].push(e);
});
return Object.keys(groups).map(titulo => ({ titulo, dados: groups[titulo] })).sort((a, b) => b.titulo.localeCompare(a.titulo));
}, [estagios, searchMain]);
const alunosAgrupados = useMemo(() => {
const groups: Record<string, Aluno[]> = {};
alunos.forEach(a => {
const key = `${a.ano}º ${a.turma_curso}`.trim();
if (!groups[key]) groups[key] = [];
groups[key].push(a);
});
return groups;
}, [alunos]);
const empresasAgrupadas = useMemo(() => {
const groups: Record<string, Empresa[]> = {};
empresas.forEach(e => {
const curso = (e.curso || 'Geral').trim().toUpperCase();
if (!groups[curso]) groups[curso] = [];
groups[curso].push(e);
});
return groups;
}, [empresas]);
return (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
{/* Header */}
{/* HEADER EPVC */}
<View style={styles.header}>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={cores.texto}/>
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={cores.azul}/>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Estágios</Text>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} onPress={fetchDados}>
<Ionicons name="refresh" size={20} color={cores.azul}/>
<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}>
<Ionicons name="reload-outline" size={20} color={cores.azul}/>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]}>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]} showsVerticalScrollIndicator={false}>
{/* SEARCH MODERNO */}
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.secundario} />
<TextInput placeholder="Procurar estagiário..." placeholderTextColor={cores.secundario} style={[styles.searchInput, { color: cores.texto }]} onChangeText={setSearchMain} />
<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}
/>
</View>
{estagiosFiltrados.map(grupo => (
{loading ? (
<ActivityIndicator size="large" color={cores.azul} style={{marginTop: 50}} />
) : estagiosFiltrados.map(grupo => (
<View key={grupo.titulo} style={{ marginBottom: 25 }}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionIndicator, { backgroundColor: cores.azul }]} />
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{grupo.titulo}</Text>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{grupo.titulo}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
{grupo.dados.map(e => (
<View key={e.id} style={[styles.estagioCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<TouchableOpacity
style={{ flex: 1 }}
<TouchableOpacity
key={e.id}
activeOpacity={0.8}
style={[styles.estagioCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => {
setEditandoEstagio(e);
setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null);
setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null);
setDataInicio(e.data_inicio || ''); setDataFim(e.data_fim || '');
carregarHorariosEdicao(e.id);
setPasso(2); setModalVisible(true);
}}
onLongPress={() => prepararApagar(e.id, e.alunos?.nome)}
>
onLongPress={() => {
setEstagioParaApagar({ id: e.id, nome: e.alunos?.nome });
setDeleteModalVisible(true);
}}
>
<View style={styles.cardHeader}>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>{e.alunos?.nome.charAt(0)}</Text>
</View>
<View style={{flex:1}}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{e.alunos?.nome}</Text>
<Text style={[styles.turmaNome, { color: cores.secundario }]}>{e.alunos?.turma_curso}</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>
</View>
</View>
<View style={[styles.timeBadge, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Ionicons name="time-outline" size={12} color={cores.azul} />
<Text style={[styles.timeText, { color: cores.texto }]}>{e.horas_diarias}</Text>
</View>
<Ionicons name="ellipsis-vertical" size={18} color={cores.secundario} />
</View>
<View style={styles.cardDetails}>
<View style={styles.detailItem}><Ionicons name="business" size={14} color={cores.azul} /><Text style={[styles.detailText, { color: cores.texto }]} numberOfLines={1}>{e.empresas?.nome}</Text></View>
<View style={styles.detailItem}><Ionicons name="time" size={14} color={cores.azul} /><Text style={[styles.detailText, { color: cores.texto }]}>{e.horas_diarias}/dia</Text></View>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
))}
</View>
))}
</ScrollView>
<TouchableOpacity style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]} onPress={() => { setPasso(1); setModalVisible(true); }}>
<Ionicons name="add" size={30} color="#fff" />
<Text style={styles.fabText}>Novo Estágio</Text>
{/* FAB EPVC */}
<TouchableOpacity
style={[styles.fab, { backgroundColor: cores.laranja, bottom: insets.bottom + 20 }]}
onPress={() => { setPasso(1); setModalVisible(true); }}
>
<Ionicons name="add" size={28} color="#fff" />
<Text style={styles.fabText}>Novo Plano</Text>
</TouchableOpacity>
</SafeAreaView>
{/* --- Modal Principal (Cadastro/Edição) --- */}
{/* MODAL DE CRIAÇÃO/EDIÇÃO */}
<Modal visible={modalVisible} animationType="slide" transparent>
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
<View style={[styles.modalContent, { backgroundColor: cores.card, paddingBottom: insets.bottom + 20 }]}>
<View style={styles.modalIndicator} />
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>{passo === 1 ? "Vincular Dados" : "Detalhes do Estágio"}</Text>
<TouchableOpacity onPress={handleFecharModal} style={[styles.closeBtn, {backgroundColor: cores.fundo}]}><Ionicons name="close" size={22} color={cores.texto} /></TouchableOpacity>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>{passo === 1 ? "Vinculação" : "Configuração"}</Text>
<Text style={[styles.modalSub, { color: cores.laranja }]}>Passo {passo} de 2</Text>
</View>
<TouchableOpacity onPress={handleFecharModal} style={[styles.closeBtn, {backgroundColor: cores.fundo}]}>
<Ionicons name="close" size={22} color={cores.texto} />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false} nestedScrollEnabled>
{passo === 1 ? (
<View style={{ gap: 20 }}>
<Text style={styles.inputLabel}>Estagiário</Text>
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 200 }]}>
<Text style={styles.inputLabel}>Selecionar Estagiário</Text>
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 220 }]}>
<ScrollView nestedScrollEnabled>
{Object.entries(alunosAgrupados).map(([turma, lista]) => (
{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.azulSuave }]}><Text style={[styles.groupHeaderText, { color: cores.azul }]}>{turma}</Text></View>
<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)}>
<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>
@@ -304,15 +329,25 @@ export default function Estagios() {
</ScrollView>
</View>
<Text style={styles.inputLabel}>Empresa</Text>
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 200 }]}>
<Text style={styles.inputLabel}>Entidade de Acolhimento</Text>
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 220 }]}>
<ScrollView nestedScrollEnabled>
{Object.entries(empresasAgrupadas).map(([curso, lista]) => (
{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)}>
<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>
@@ -323,69 +358,75 @@ export default function Estagios() {
) : (
<View style={{ gap: 18 }}>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={styles.groupTitle}>Duração</Text>
<Text style={[styles.groupTitle, { color: cores.azul }]}>Cronograma</Text>
<View style={styles.rowInputs}>
<View style={{flex:1}}><Text style={styles.miniLabel}>INÍCIO</Text><TextInput style={[styles.modernInput, { color: cores.texto }]} value={dataInicio} onChangeText={setDataInicio} placeholder="YYYY-MM-DD"/></View>
<View style={{flex:1}}><Text style={styles.miniLabel}>FIM</Text><TextInput style={[styles.modernInput, { color: cores.texto }]} value={dataFim} onChangeText={setDataFim} placeholder="YYYY-MM-DD"/></View>
<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"/></View>
<View style={{flex:1}}><Text style={styles.miniLabel}>DATA FIM (PREV.)</Text><TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataFim} onChangeText={setDataFim} placeholder="AAAA-MM-DD"/></View>
</View>
</View>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={styles.groupTitle}>Horários</Text>
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<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>
<View style={styles.tabelaRow}>
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Manhã</Text>
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hManhaIni} onChangeText={setHManhaIni} placeholder="09:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hManhaFim} onChangeText={setHManhaFim} placeholder="13:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hManhaIni} onChangeText={setHManhaIni} placeholder="09:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hManhaFim} onChangeText={setHManhaFim} placeholder="13:00" />
</View>
<View style={styles.tabelaRow}>
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Tarde</Text>
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hTardeIni} onChangeText={setHTardeIni} placeholder="14:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hTardeFim} onChangeText={setHTardeFim} placeholder="18:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hTardeIni} onChangeText={setHTardeIni} placeholder="14:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hTardeFim} onChangeText={setHTardeFim} placeholder="18:00" />
</View>
<View style={[styles.totalBadge, { backgroundColor: cores.azul }]}><Text style={styles.totalText}>Total: {totalHorasDiarias}/dia</Text></View>
</View>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={styles.groupTitle}>Tutor</Text>
<TextInput style={[styles.modernInput, {color: cores.texto}]} 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, marginTop: 10}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Contacto"/>
<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"/>
</View>
</View>
)}
</ScrollView>
<View style={styles.modalFooter}>
{passo === 2 && <TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.fundo }]}><Text style={{color: cores.texto, fontWeight: '700'}}>VOLTAR</Text></TouchableOpacity>}
{passo === 2 && (
<TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { borderColor: cores.borda }]}>
<Text style={{color: cores.texto, fontWeight: '800'}}>VOLTAR</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => {
if(passo === 1) {
if(alunoSelecionado && empresaSelecionada) setPasso(2);
else Alert.alert("Vai dar merda", "Seleciona o aluno e a empresa primeiro!");
else Alert.alert("Atenção", "Selecione o aluno e a empresa.");
} else salvarEstagio();
}}
style={[styles.btnModalPri, { backgroundColor: cores.azul }]}
>
<Text style={{color:'#fff', fontWeight: '900'}}>{passo === 1 ? "PRÓXIMO" : "FINALIZAR"}</Text>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={{color:'#fff', fontWeight: '900'}}>{passo === 1 ? "CONTINUAR" : "FINALIZAR"}</Text>}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
{/* --- NOVO MODAL DE ELIMINAÇÃO MODERNIZADO --- */}
{/* DELETE MODAL */}
<Modal visible={deleteModalVisible} transparent animationType="fade">
<View style={[styles.modalOverlay, { backgroundColor: 'rgba(0,0,0,0.7)' }]}>
<View style={[styles.deleteCard, { backgroundColor: cores.card }]}>
<View style={[styles.deleteIconBg, { backgroundColor: cores.vermelho + '20' }]}>
<Ionicons name="trash-bin" size={32} color={cores.vermelho} />
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
<View style={[styles.deleteCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.deleteIconBg, { backgroundColor: isDarkMode ? '#2D1616' : '#FFF5F5' }]}>
<Ionicons name="warning" size={35} color={cores.vermelho} />
</View>
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Eliminar Estágio?</Text>
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Cancelar Estágio?</Text>
<Text style={[styles.deleteSubtitle, { color: cores.secundario }]}>
Estás prestes a apagar o registo de <Text style={{fontWeight: '900', color: cores.texto}}>{estagioParaApagar?.nome}</Text>. Esta ação é irreversível.
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 }]}>CANCELAR</Text>
<Text style={[styles.deleteBtnText, { color: cores.texto }]}>FECHAR</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.deleteBtnConfirm, { backgroundColor: cores.vermelho }]} onPress={confirmarEliminacao}>
<Text style={[styles.deleteBtnText, { color: '#fff' }]}>ELIMINAR</Text>
@@ -399,59 +440,60 @@ export default function Estagios() {
}
const styles = StyleSheet.create({
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
headerTitle: { fontSize: 18, fontWeight: '900' },
btnAction: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
scroll: { paddingHorizontal: 20, paddingTop: 10 },
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 52, borderRadius: 16, borderWidth: 1, marginBottom: 20 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15, marginLeft: 5 },
sectionIndicator: { width: 4, height: 16, borderRadius: 2, marginRight: 8 },
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
estagioCard: { padding: 16, borderRadius: 24, borderWidth: 1, marginBottom: 12 },
cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 12 },
avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
avatarText: { fontSize: 18, fontWeight: '900' },
alunoNome: { fontSize: 15, fontWeight: '800' },
turmaNome: { fontSize: 12, fontWeight: '600' },
cardDetails: { flexDirection: 'row', gap: 15 },
detailItem: { flexDirection: 'row', alignItems: 'center', gap: 6, flexShrink: 1 },
detailText: { fontSize: 12, fontWeight: '700' },
fab: { position: 'absolute', right: 20, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 15, borderRadius: 20, elevation: 8 },
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 8 },
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },
modalContent: { borderTopLeftRadius: 35, borderTopRightRadius: 35, padding: 25, height: '90%', width: '100%', marginTop: 'auto' },
modalIndicator: { width: 40, height: 5, backgroundColor: '#E2E8F0', borderRadius: 10, alignSelf: 'center', marginBottom: 15 },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
modalTitle: { fontSize: 20, fontWeight: '900' },
closeBtn: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 8, marginLeft: 5 },
pickerContainer: { borderWidth: 1, borderRadius: 20, overflow: 'hidden' },
groupHeader: { paddingVertical: 8, paddingHorizontal: 15, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
groupHeaderText: { fontSize: 10, fontWeight: '900' },
pickerItem: { padding: 16, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
pickerItemText: { fontSize: 14, fontWeight: '600' },
modernGroup: { padding: 18, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
groupTitle: { fontSize: 13, fontWeight: '900', color: '#2390a6', marginBottom: 10 },
rowInputs: { flexDirection: 'row', gap: 12 },
miniLabel: { fontSize: 10, fontWeight: '800', color: '#94A3B8', marginBottom: 5 },
modernInput: { paddingVertical: 10, paddingHorizontal: 15, borderRadius: 12, backgroundColor: 'rgba(0,0,0,0.03)', fontSize: 14, fontWeight: '700' },
tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
tabelaLabel: { flex: 1.2, fontSize: 13, fontWeight: '700' },
tabelaInput: { flex: 1, padding: 12, borderRadius: 12, backgroundColor: 'rgba(0,0,0,0.04)', textAlign: 'center', fontSize: 14, fontWeight: '700' },
totalBadge: { alignSelf: 'flex-start', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, marginTop: 5 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
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' },
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 },
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 },
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase', letterSpacing: 0.5 },
modalOverlay: { flex: 1, justifyContent: 'flex-end' },
modalContent: { borderTopLeftRadius: 45, borderTopRightRadius: 45, padding: 28, 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 },
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 },
pickerContainer: { borderWidth: 1.5, borderRadius: 24, overflow: 'hidden' },
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 },
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)' },
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)' },
totalBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
totalText: { color: '#fff', fontSize: 11, fontWeight: '900' },
modalFooter: { flexDirection: 'row', gap: 15, marginTop: 10 },
btnModalPri: { flex: 2, height: 58, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
btnModalSec: { flex: 1, height: 58, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
// Estilos do Delete Modal
deleteCard: { width: '85%', borderRadius: 30, padding: 25, alignItems: 'center' },
deleteIconBg: { width: 70, height: 70, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
deleteTitle: { fontSize: 20, fontWeight: '900', marginBottom: 10 },
deleteSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 20, marginBottom: 25 },
deleteFooter: { flexDirection: 'row', gap: 12, width: '100%' },
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' },
deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12 },
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' },
deleteBtnText: { fontSize: 13, fontWeight: '900' },
deleteBtnText: { fontSize: 14, fontWeight: '900' },
});

View File

@@ -1,5 +1,6 @@
// app/(Admin)/FaltasAlunos.tsx
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
@@ -37,7 +38,6 @@ interface Falta {
const FaltasAlunos = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const params = useLocalSearchParams();
const [search, setSearch] = useState('');
const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]);
@@ -48,32 +48,23 @@ const FaltasAlunos = memo(() => {
const [faltas, setFaltas] = useState<Falta[]>([]);
const [loadingFaltas, setLoadingFaltas] = useState(false);
const azulPetroleo = '#2390a6';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: '#3B82F6',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
vermelho: '#EF4444',
verde: '#10B981',
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
}), [isDarkMode]);
useEffect(() => { fetchAlunos(); }, []);
useEffect(() => {
if (params.alunoId && typeof params.alunoId === 'string') {
const alunoAuto = {
id: params.alunoId,
nome: (params.nome as string) || 'Aluno',
n_escola: '',
turma: '',
};
abrirFaltas(alunoAuto);
}
}, [params.alunoId]);
const fetchAlunos = async () => {
try {
setLoading(true);
@@ -89,47 +80,85 @@ const FaltasAlunos = memo(() => {
data?.forEach(item => {
const nomeTurma = `${item.ano}º ${item.turma_curso}`;
if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = [];
agrupadas[nomeTurma].push({ id: item.id, nome: item.nome, n_escola: item.n_escola, turma: nomeTurma });
agrupadas[nomeTurma].push({
id: item.id,
nome: item.nome,
n_escola: item.n_escola,
turma: nomeTurma
});
});
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
} catch (err) { console.error(err); } finally { setLoading(false); }
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const abrirFaltas = async (aluno: Aluno) => {
setAlunoSelecionado(aluno);
setModalFaltasVisible(true);
setLoadingFaltas(true);
setFaltas([]);
try {
const hoje = new Date().toISOString().split('T')[0];
// CORREÇÃO DO ERRO PGRST116: Usamos .limit(1) em vez de .single()
const { data: listaEstagios, error: errEstagio } = await supabase
.from('estagios')
.select('data_inicio, data_fim')
.lte('data_inicio', hoje)
.gte('data_fim', hoje)
.limit(1); // Pega apenas o primeiro se houver múltiplos
if (errEstagio) throw errEstagio;
// Se não houver estágio hoje, tentamos pegar o mais recente
let inicio = '2026-01-01';
let fim = '2026-12-31';
if (listaEstagios && listaEstagios.length > 0) {
inicio = listaEstagios[0].data_inicio;
fim = listaEstagios[0].data_fim;
}
const { data, error } = await supabase
.from('presencas')
.select('id, data, justificacao_url, estado')
.eq('aluno_id', aluno.id)
.eq('estado', 'faltou')
.gte('data', inicio)
.lte('data', fim)
.order('data', { ascending: false });
if (error) throw error;
setFaltas(data || []);
} catch (err) { console.error(err); } finally { setLoadingFaltas(false); }
} catch (err) {
console.error("Erro na base:", err);
Alert.alert("Erro", "Não foi possível carregar as faltas deste estágio.");
} finally {
setLoadingFaltas(false);
}
};
const verDocumento = async (url: string) => {
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert("Erro", "Não foi possível abrir o link do documento.");
}
await Linking.openURL(url);
} catch (error) {
Alert.alert("Erro", "URL inválida ou documento inacessível.");
Alert.alert("Erro", "Não foi possível abrir o comprovativo.");
}
};
const filteredTurmas = turmas
.map(turma => ({
...turma,
alunos: turma.alunos.filter(a => a.nome.toLowerCase().includes(search.toLowerCase()) || a.n_escola.includes(search)),
alunos: turma.alunos.filter(a =>
a.nome.toLowerCase().includes(search.toLowerCase()) ||
a.n_escola.includes(search)
),
}))
.filter(t => t.alunos.length > 0);
@@ -139,20 +168,17 @@ const FaltasAlunos = memo(() => {
<View style={styles.headerFixed}>
<View style={styles.topBar}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<Ionicons name="chevron-back" size={22} color={cores.texto} />
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Gestão de Faltas</Text>
<View style={{ width: 42 }} />
<Text style={[styles.topTitle, { color: cores.texto }]}>Gestão de Faltas</Text>
<View style={{ width: 42 }} />
</View>
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.secundario} />
<View style={[styles.searchContainer, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
placeholder="Procurar aluno..."
placeholder="Procurar aluno ou nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
@@ -173,25 +199,19 @@ const FaltasAlunos = memo(() => {
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.section}>
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.turmaLabel, { color: cores.azul }]}>{item.nome}</Text>
</View>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>{item.nome}</Text>
{item.alunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.7}
style={[styles.card, { backgroundColor: cores.card }]}
onPress={() => abrirFaltas(aluno)}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>{aluno.nome.charAt(0)}</Text>
<TouchableOpacity key={aluno.id} activeOpacity={0.7} style={[styles.card, { backgroundColor: cores.card }]} onPress={() => abrirFaltas(aluno)}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Text style={styles.avatarText}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.info}>
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.subText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
<Text style={[styles.subText, { color: cores.secundario }]}> Aluno: {aluno.n_escola}</Text>
</View>
<View style={[styles.alertIcon, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="alert-circle" size={18} color={cores.vermelho} />
</View>
<Ionicons name="alert-circle-outline" size={20} color={cores.vermelho} />
</TouchableOpacity>
))}
</View>
@@ -199,24 +219,16 @@ const FaltasAlunos = memo(() => {
/>
)}
<Modal
visible={modalFaltasVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setModalFaltasVisible(false)}
>
<Modal visible={modalFaltasVisible} animationType="slide" transparent onRequestClose={() => setModalFaltasVisible(false)}>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<View style={[styles.modalHeader, { borderBottomColor: cores.borda }]}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={[styles.modalHeader, { backgroundColor: cores.card }]}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Histórico de Faltas</Text>
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>{alunoSelecionado?.nome}</Text>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Faltas de Estágio</Text>
<Text style={[styles.modalSubtitle, { color: cores.azul }]}>{alunoSelecionado?.nome}</Text>
</View>
<TouchableOpacity
onPress={() => setModalFaltasVisible(false)}
style={[styles.closeBtn, { backgroundColor: cores.fundo }]}
>
<Ionicons name="close" size={22} color={cores.texto} />
<TouchableOpacity onPress={() => setModalFaltasVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
</View>
@@ -226,41 +238,32 @@ const FaltasAlunos = memo(() => {
<ScrollView contentContainerStyle={{ padding: 20 }} showsVerticalScrollIndicator={false}>
{faltas.length === 0 ? (
<View style={styles.emptyState}>
<Ionicons name="checkmark-circle-outline" size={48} color={cores.verde} style={{ opacity: 0.3 }} />
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhuma falta registada.</Text>
<View style={[styles.emptyIconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="checkmark-done" size={40} color={cores.azul} />
</View>
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem faltas no período atual</Text>
</View>
) : (
faltas.map(f => (
<View key={f.id} style={[styles.faltaCard, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<View style={styles.faltaHeader}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Ionicons name="calendar-outline" size={16} color={cores.azul} style={{ marginRight: 8 }} />
<Text style={[styles.faltaData, { color: cores.texto }]}>
{new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })}
</Text>
<View key={f.id} style={[styles.faltaCard, { backgroundColor: cores.card }]}>
<View style={styles.faltaRow}>
<View style={[styles.dateBadge, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="calendar" size={18} color={cores.azul} />
</View>
<View style={[styles.statusBadge, { backgroundColor: f.justificacao_url ? cores.verde + '20' : cores.vermelho + '20' }]}>
<Text style={[styles.statusText, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.faltaData, { color: cores.texto }]}>
{new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })}
</Text>
<Text style={[styles.faltaStatus, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
{f.justificacao_url ? 'Justificada' : 'Injustificada'}
</Text>
</View>
{f.justificacao_url && (
<TouchableOpacity style={[styles.viewBtn, { backgroundColor: cores.azul }]} onPress={() => verDocumento(f.justificacao_url!)}>
<Ionicons name="eye-outline" size={18} color="#fff" />
</TouchableOpacity>
)}
</View>
{f.justificacao_url ? (
<TouchableOpacity
activeOpacity={0.8}
style={[styles.pdfBtn, { backgroundColor: cores.azulSuave }]}
onPress={() => verDocumento(f.justificacao_url!)}
>
<Ionicons name="document-attach" size={18} color={cores.azul} />
<Text style={[styles.pdfBtnText, { color: cores.azul }]}>Ver Comprovativo</Text>
</TouchableOpacity>
) : (
<View style={styles.noJustifContainer}>
<Ionicons name="warning-outline" size={14} color={cores.vermelho} />
<Text style={[styles.noJustifText, { color: cores.vermelho }]}>Aguardando justificação</Text>
</View>
)}
</View>
))
)}
@@ -276,44 +279,37 @@ const FaltasAlunos = memo(() => {
export default FaltasAlunos;
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 },
headerFixed: { paddingHorizontal: 20, paddingBottom: 15 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 70 },
backBtnPremium: {
width: 42, height: 42, borderRadius: 14,
justifyContent: 'center', alignItems: 'center',
borderWidth: 1, elevation: 2, shadowColor: '#000',
shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2
},
title: { fontSize: 22, fontWeight: '800' },
searchBox: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 15, paddingHorizontal: 15, height: 50, marginTop: 10 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
headerFixed: { paddingHorizontal: 20, paddingBottom: 10 },
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', height: 60, marginTop: 10 },
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
topTitle: { fontSize: 18, fontWeight: '800' },
searchContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 16, borderWidth: 1.5, marginTop: 10 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 30, paddingTop: 10 },
section: { marginBottom: 25 },
turmaBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 },
turmaLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.8 },
card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 22, marginBottom: 10, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 },
avatar: { width: 46, height: 46, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
avatarText: { fontSize: 18, fontWeight: '800' },
sectionLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 5, letterSpacing: 1.2 },
card: { flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 24, marginBottom: 12, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 },
avatar: { width: 50, height: 50, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },
avatarText: { color: '#fff', fontSize: 22, fontWeight: '800' },
info: { flex: 1, marginLeft: 15 },
nome: { fontSize: 16, fontWeight: '700' },
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
subText: { fontSize: 12, fontWeight: '600', marginTop: 3 },
alertIcon: { width: 34, height: 34, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
modalContent: { height: '85%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, borderBottomWidth: 1 },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' },
modalContent: { height: '82%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25 },
modalTitle: { fontSize: 20, fontWeight: '800' },
modalSubtitle: { fontSize: 14, fontWeight: '600', marginTop: 2 },
closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
faltaCard: { padding: 16, borderRadius: 20, marginBottom: 15, borderWidth: 1 },
faltaHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
faltaData: { fontSize: 15, fontWeight: '700' },
statusBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
statusText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
pdfBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: 12, padding: 12 },
pdfBtnText: { marginLeft: 10, fontSize: 14, fontWeight: '700' },
noJustifContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 5 },
noJustifText: { marginLeft: 6, fontSize: 12, fontWeight: '600', fontStyle: 'italic' },
emptyState: { alignItems: 'center', marginTop: 50 },
emptyText: { textAlign: 'center', marginTop: 15, fontSize: 14, fontWeight: '600' }
modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 },
closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
faltaCard: { padding: 16, borderRadius: 22, marginBottom: 12, elevation: 1 },
faltaRow: { flexDirection: 'row', alignItems: 'center' },
dateBadge: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
faltaData: { fontSize: 15, fontWeight: '800' },
faltaStatus: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 },
viewBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
emptyState: { alignItems: 'center', marginTop: 80 },
emptyIconContainer: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
emptyText: { fontSize: 16, fontWeight: '700' }
});

View File

@@ -4,6 +4,7 @@ import { useRouter } from 'expo-router';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
FlatList,
RefreshControl,
StatusBar,
@@ -23,6 +24,7 @@ export interface Aluno {
nome: string;
n_escola: string;
turma: string;
tem_estagio?: boolean;
}
interface TurmaAgrupada {
@@ -40,16 +42,19 @@ const ListaAlunosProfessor = memo(() => {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const azulPetroleo = '#2390a6';
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
}), [isDarkMode]);
const fetchAlunos = async () => {
@@ -57,7 +62,14 @@ const ListaAlunosProfessor = memo(() => {
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,
estagios(id)
`)
.order('ano', { ascending: false })
.order('nome', { ascending: true });
@@ -66,17 +78,21 @@ const ListaAlunosProfessor = memo(() => {
const agrupadas: Record<string, Aluno[]> = {};
data.forEach(item => {
const nomeTurma = `${item.ano}º ${item.turma_curso}`;
const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase();
if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = [];
agrupadas[nomeTurma].push({
id: item.id,
nome: item.nome,
n_escola: item.n_escola,
turma: nomeTurma,
tem_estagio: item.estagios && item.estagios.length > 0
});
});
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
setTurmas(Object.keys(agrupadas)
.sort((a, b) => b.localeCompare(a))
.map(nome => ({ nome, alunos: agrupadas[nome] }))
);
} catch (err) {
console.error('Erro:', err);
} finally {
@@ -110,37 +126,35 @@ const ListaAlunosProfessor = memo(() => {
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER IGUAL ÀS EMPRESAS */}
{/* HEADER EPVC STYLE */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: 15 }}>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Alunos</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
Gestão de turmas e estágios
</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Gestão de Turmas</Text>
</View>
<TouchableOpacity
style={[styles.refreshBtn, { backgroundColor: cores.azul }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={fetchAlunos}
>
<Ionicons name="reload" size={22} color="#fff" />
<Ionicons name="reload-outline" size={20} color={cores.azul} />
</TouchableOpacity>
</View>
{/* SEARCH BAR IGUAL ÀS EMPRESAS */}
{/* SEARCH MODERNO */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.azul} />
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar por nome ou nº..."
placeholder="Pesquisar por aluno ou nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
@@ -149,20 +163,18 @@ const ListaAlunosProfessor = memo(() => {
</View>
{loading && !refreshing ? (
<View style={styles.loadingCenter}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 50 }} />
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 100 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderItem={({ item }) => (
<View style={{ marginBottom: 25 }}>
{/* Cabeçalho da Secção (Turma) */}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{item.nome}</Text>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
@@ -176,19 +188,31 @@ const ListaAlunosProfessor = memo(() => {
params: { alunoId: aluno.id }
})}
>
<View style={[styles.alunoIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="person" size={22} color={cores.azul} />
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>
{aluno.nome.charAt(0)}
</Text>
</View>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<View style={styles.idRow}>
<Ionicons name="id-card-outline" size={14} color={cores.secundario} />
<Text style={[styles.idText, { color: cores.secundario }]}> Escola: {aluno.n_escola}</Text>
<Ionicons name="finger-print-outline" size={13} color={cores.secundario} />
<Text style={[styles.idText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.4} />
{aluno.tem_estagio ? (
<View style={[styles.statusBadge, { backgroundColor: cores.verde + '20' }]}>
<Ionicons name="checkmark-circle" size={12} color={cores.verde} />
<Text style={[styles.statusText, { color: cores.verde }]}>COLOCADO</Text>
</View>
) : (
<View style={[styles.statusBadge, { backgroundColor: cores.laranja + '20' }]}>
<Ionicons name="alert-circle" size={12} color={cores.laranja} />
<Text style={[styles.statusText, { color: cores.laranja }]}>PENDENTE</Text>
</View>
)}
</TouchableOpacity>
))}
</View>
@@ -196,11 +220,20 @@ const ListaAlunosProfessor = memo(() => {
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="people-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '600' }}>Nenhum aluno encontrado.</Text>
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Nenhum aluno encontrado.</Text>
</View>
)}
/>
)}
{/* FAB */}
<TouchableOpacity
style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]}
onPress={() => Alert.alert("EPVC", "Funcionalidade de registo de aluno em desenvolvimento.")}
>
<Ionicons name="person-add" size={24} color="#fff" />
<Text style={styles.fabText}>Novo Aluno</Text>
</TouchableOpacity>
</SafeAreaView>
</View>
);
@@ -208,26 +241,30 @@ const ListaAlunosProfessor = memo(() => {
const styles = StyleSheet.create({
safe: { flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 13, fontWeight: '600' },
refreshBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 5 },
searchSection: { paddingHorizontal: 20, marginVertical: 15 },
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 18, borderWidth: 1.5 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' },
loadingCenter: { marginTop: 50, alignItems: 'center' },
emptyContainer: { marginTop: 80, alignItems: 'center' },
listPadding: { paddingHorizontal: 20 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 15 },
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 },
sectionLine: { flex: 1, height: 1, borderRadius: 1 },
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 },
alunoIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
searchSection: { paddingHorizontal: 24, marginBottom: 10 },
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 },
searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' },
listPadding: { paddingHorizontal: 24, paddingTop: 10 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 },
sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 },
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 },
sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 },
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 },
avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
avatarText: { fontSize: 18, fontWeight: '900' },
alunoInfo: { flex: 1, marginLeft: 15 },
alunoNome: { fontSize: 16, fontWeight: '800' },
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 },
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' },
fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 },
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase' },
});
export default ListaAlunosProfessor;

View File

@@ -1,3 +1,4 @@
// app/(Professor)/Presencas.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
@@ -31,14 +32,16 @@ const Presencas = memo(() => {
const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]);
const [loading, setLoading] = useState(true);
// Paleta de Cores Premium
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: '#3B82F6',
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9',
card: isDarkMode ? '#161618' : '#FFFFFF',
texto: isDarkMode ? '#FFFFFF' : '#1A1C1E',
secundario: isDarkMode ? '#8E8E93' : '#6C757D',
azul: '#2390a6', // O teu azul de estimação
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
borda: isDarkMode ? '#2C2C2E' : '#E9ECEF',
sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)',
}), [isDarkMode]);
useEffect(() => {
@@ -100,54 +103,65 @@ const Presencas = memo(() => {
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
{/* HEADER PREMIUM */}
<View style={styles.headerFixed}>
<View style={styles.topBar}>
{/* HEADER DINÂMICO */}
<View style={styles.headerContainer}>
<View style={styles.topRow}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<Ionicons name="chevron-back" size={22} color={cores.texto} />
<Ionicons name="arrow-back" size={20} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Presenças</Text>
<View style={{ width: 42 }} />
<View style={styles.titleWrapper}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Presenças</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>Lista de Alunos</Text>
</View>
<View style={{ width: 45 }} />
</View>
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.secundario} />
<View style={[styles.searchWrapper, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={18} color={cores.azul} />
<TextInput
placeholder="Procurar aluno ou Nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
style={[styles.searchInput, { color: cores.texto }]}
style={[styles.inputSearch, { color: cores.texto }]}
/>
{search !== '' && (
<TouchableOpacity onPress={() => setSearch('')}>
<Ionicons name="close-circle" size={18} color={cores.secundario} />
</TouchableOpacity>
)}
</View>
</View>
{loading ? (
<View style={styles.centered}>
<View style={styles.loaderArea}>
<ActivityIndicator size="large" color={cores.azul} />
<Text style={[styles.loaderText, { color: cores.secundario }]}>A carregar turma...</Text>
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={styles.scrollContent}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.section}>
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.turmaLabel, { color: cores.azul }]}>
{item.nome} {item.alunos.length} Alunos
</Text>
<View style={styles.turmaSection}>
<View style={styles.turmaHeader}>
<View style={[styles.turmaDot, { backgroundColor: cores.azul }]} />
<Text style={[styles.turmaTitle, { color: cores.texto }]}>{item.nome}</Text>
<View style={[styles.countBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.countText, { color: cores.azul }]}>{item.alunos.length}</Text>
</View>
</View>
{item.alunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
style={[styles.card, { backgroundColor: cores.card }]}
activeOpacity={0.7}
style={[styles.alunoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}
onPress={() =>
router.push({
pathname: '/Professor/Alunos/CalendarioPresencas',
@@ -155,18 +169,23 @@ const Presencas = memo(() => {
})
}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>
<View style={[styles.avatarBox, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarChar, { color: cores.azul }]}>
{aluno.nome.charAt(0).toUpperCase()}
</Text>
</View>
<View style={styles.info}>
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.subText, { color: cores.secundario }]}>Ver registo de presenças</Text>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{aluno.nome}</Text>
<View style={styles.alunoStatusRow}>
<Ionicons name="calendar-outline" size={12} color={cores.secundario} />
<Text style={[styles.alunoSub, { color: cores.secundario }]}> Registo de faltas</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
<View style={[styles.goCircle, { backgroundColor: cores.fundo }]}>
<Ionicons name="chevron-forward" size={16} color={cores.azul} />
</View>
</TouchableOpacity>
))}
</View>
@@ -184,77 +203,83 @@ const styles = StyleSheet.create({
flex: 1,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0,
},
headerFixed: {
paddingHorizontal: 20,
paddingBottom: 15,
headerContainer: {
paddingHorizontal: 24,
paddingBottom: 20,
paddingTop: 10,
},
topBar: {
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
height: 70,
marginBottom: 20,
},
backBtnPremium: {
width: 42,
height: 42,
borderRadius: 14,
btnBack: {
width: 45,
height: 45,
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
title: { fontSize: 22, fontWeight: '800' },
searchBox: {
titleWrapper: { alignItems: 'center' },
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 },
searchWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 15,
paddingHorizontal: 15,
height: 50,
marginTop: 10,
borderRadius: 18,
paddingHorizontal: 16,
height: 54,
},
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
section: { marginBottom: 25 },
turmaBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' },
listContent: { paddingHorizontal: 24, paddingBottom: 40 },
turmaSection: { marginBottom: 30 },
turmaHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
paddingLeft: 4
},
turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 },
turmaTitle: { fontSize: 16, fontWeight: '800', flex: 1 },
countBadge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 10,
alignSelf: 'flex-start',
marginBottom: 12,
},
turmaLabel: {
fontSize: 12,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.8
},
card: {
countText: { fontSize: 11, fontWeight: '900' },
alunoCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 14,
borderRadius: 22,
marginBottom: 10,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 10,
padding: 12,
borderRadius: 24,
marginBottom: 12,
elevation: 4,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 1,
shadowRadius: 12,
},
avatar: {
width: 46,
height: 46,
borderRadius: 15,
avatarBox: {
width: 52,
height: 52,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
},
avatarText: { fontSize: 18, fontWeight: '800' },
info: { flex: 1, marginLeft: 15 },
nome: { fontSize: 16, fontWeight: '700' },
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
avatarChar: { fontSize: 20, fontWeight: '900' },
alunoInfo: { flex: 1, marginLeft: 16 },
alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 },
alunoStatusRow: { flexDirection: 'row', alignItems: 'center' },
alunoSub: { fontSize: 13, fontWeight: '500' },
goCircle: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' },
loaderText: { marginTop: 15, fontSize: 14, fontWeight: '600' },
});

View File

@@ -1,3 +1,4 @@
// app/(Professor)/SumariosAlunos.tsx
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
@@ -46,18 +47,17 @@ const SumariosAlunos = memo(() => {
const [loadingSumarios, setLoadingSumarios] = useState(false);
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: '#3B82F6',
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9',
card: isDarkMode ? '#161618' : '#FFFFFF',
texto: isDarkMode ? '#FFFFFF' : '#1A1C1E',
secundario: isDarkMode ? '#8E8E93' : '#6C757D',
azul: '#2390a6',
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
borda: isDarkMode ? '#2C2C2E' : '#E9ECEF',
sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)',
}), [isDarkMode]);
useEffect(() => {
fetchAlunos();
}, []);
useEffect(() => { fetchAlunos(); }, []);
useEffect(() => {
if (params.alunoId && typeof params.alunoId === 'string') {
@@ -95,11 +95,7 @@ const SumariosAlunos = memo(() => {
});
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
} catch (err) {
console.error('Erro ao carregar alunos:', err);
} finally {
setLoading(false);
}
} catch (err) { console.error(err); } finally { setLoading(false); }
};
const abrirSumarios = async (aluno: Aluno) => {
@@ -107,20 +103,37 @@ const SumariosAlunos = memo(() => {
setModalVisible(true);
setLoadingSumarios(true);
try {
const hoje = new Date().toISOString().split('T')[0];
// 1. Procurar o estágio ativo
const { data: listaEstagios } = await supabase
.from('estagios')
.select('data_inicio, data_fim')
.lte('data_inicio', hoje)
.gte('data_fim', hoje)
.limit(1);
let inicio = '2026-01-01';
let fim = '2026-12-31';
if (listaEstagios && listaEstagios.length > 0) {
inicio = listaEstagios[0].data_inicio;
fim = listaEstagios[0].data_fim;
}
// 2. Buscar sumários filtrados pelo período do estágio
const { data, error } = await supabase
.from('presencas')
.select('id, data, sumario')
.eq('aluno_id', aluno.id)
.gte('data', inicio)
.lte('data', fim)
.not('sumario', 'is', null)
.order('data', { ascending: false });
if (error) throw error;
setSumarios(data || []);
} catch (err) {
console.error('Erro sumários:', err);
} finally {
setLoadingSumarios(false);
}
} catch (err) { console.error(err); } finally { setLoadingSumarios(false); }
};
const filteredTurmas = turmas
@@ -136,61 +149,67 @@ const SumariosAlunos = memo(() => {
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={styles.headerFixed}>
<View style={styles.topBar}>
<View style={styles.headerContainer}>
<View style={styles.topRow}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]}
>
<Ionicons name="chevron-back" size={22} color={cores.texto} />
<Ionicons name="arrow-back" size={20} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Sumários</Text>
<View style={{ width: 42 }} />
<View style={styles.titleWrapper}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Sumários</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>Estágio Atual</Text>
</View>
<View style={{ width: 45 }} />
</View>
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.secundario} />
<View style={[styles.searchWrapper, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={18} color={cores.azul} />
<TextInput
placeholder="Procurar aluno..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
style={[styles.searchInput, { color: cores.texto }]}
style={[styles.inputSearch, { color: cores.texto }]}
/>
</View>
</View>
{loading ? (
<View style={styles.centered}>
<View style={styles.loaderArea}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={styles.scrollContent}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.section}>
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.turmaLabel, { color: cores.azul }]}>{item.nome}</Text>
<View style={styles.turmaSection}>
<View style={styles.turmaHeader}>
<View style={[styles.turmaDot, { backgroundColor: cores.azul }]} />
<Text style={[styles.turmaTitle, { color: cores.texto }]}>{item.nome}</Text>
</View>
{item.alunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.7}
style={[styles.card, { backgroundColor: cores.card }]}
style={[styles.alunoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}
onPress={() => abrirSumarios(aluno)}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { color: cores.azul }]}>{aluno.nome.charAt(0)}</Text>
<View style={[styles.avatarBox, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarChar, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.info}>
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.subText, { color: cores.secundario }]}> {aluno.n_escola}</Text>
<View style={styles.alunoInfo}>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.alunoSub, { color: cores.secundario }]}> {aluno.n_escola}</Text>
</View>
<View style={[styles.goCircle, { backgroundColor: cores.fundo }]}>
<Ionicons name="reader-outline" size={16} color={cores.azul} />
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</TouchableOpacity>
))}
</View>
@@ -198,46 +217,45 @@ const SumariosAlunos = memo(() => {
/>
)}
<Modal
visible={modalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setModalVisible(false)}
>
<Modal visible={modalVisible} animationType="slide" transparent={true} onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<View style={[styles.modalHeader, { borderBottomColor: cores.borda }]}>
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
<View style={[styles.modalHeader, { backgroundColor: cores.card }]}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Sumários</Text>
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>{alunoSelecionado?.nome}</Text>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Caderno de Sumários</Text>
<Text style={[styles.modalSubtitle, { color: cores.azul }]}>{alunoSelecionado?.nome}</Text>
</View>
<TouchableOpacity
onPress={() => setModalVisible(false)}
style={[styles.closeBtn, { backgroundColor: cores.fundo }]}
>
<Ionicons name="close" size={22} color={cores.texto} />
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="close" size={24} color={cores.azul} />
</TouchableOpacity>
</View>
{loadingSumarios ? (
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
) : (
<ScrollView contentContainerStyle={{ padding: 20 }} showsVerticalScrollIndicator={false}>
<ScrollView contentContainerStyle={styles.modalScroll} showsVerticalScrollIndicator={false}>
{sumarios.length === 0 ? (
<View style={styles.emptyState}>
<Ionicons name="document-text-outline" size={48} color={cores.secundario} style={{ opacity: 0.3 }} />
<Text style={[styles.emptyText, { color: cores.secundario }]}>Sem registos de sumários.</Text>
<View style={styles.emptyContainer}>
<View style={[styles.emptyIconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="receipt-outline" size={40} color={cores.azul} />
</View>
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem sumários neste estágio</Text>
<Text style={[styles.emptySub, { color: cores.secundario }]}>Não foram encontrados registos para o período atual.</Text>
</View>
) : (
sumarios.map(s => (
<View key={s.id} style={[styles.sumarioCard, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<View style={styles.sumarioHeader}>
<Ionicons name="calendar-outline" size={14} color={cores.azul} />
<Text style={[styles.sumarioData, { color: cores.azul }]}>
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })}
</Text>
<View key={s.id} style={[styles.sumarioCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}>
<View style={styles.sumarioHeaderRow}>
<View style={[styles.dateBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.dateText, { color: cores.azul }]}>
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
</Text>
</View>
<View style={styles.sumarioLine} />
</View>
<View style={styles.sumarioBody}>
<Text style={[styles.sumarioTexto, { color: cores.texto }]}>{s.sumario}</Text>
</View>
<Text style={[styles.sumarioTexto, { color: cores.texto }]}>{s.sumario}</Text>
</View>
))
)}
@@ -254,38 +272,43 @@ export default SumariosAlunos;
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 },
headerFixed: { paddingHorizontal: 20, paddingBottom: 15 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 70 },
backBtnPremium: {
width: 42, height: 42, borderRadius: 14,
justifyContent: 'center', alignItems: 'center',
borderWidth: 1, elevation: 2, shadowColor: '#000',
shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2
},
title: { fontSize: 22, fontWeight: '800' },
searchBox: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 15, paddingHorizontal: 15, height: 50, marginTop: 10 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
section: { marginBottom: 25 },
turmaBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 },
turmaLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.8 },
card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 22, marginBottom: 10, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 },
avatar: { width: 46, height: 46, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
avatarText: { fontSize: 18, fontWeight: '800' },
info: { flex: 1, marginLeft: 15 },
nome: { fontSize: 16, fontWeight: '700' },
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
modalContent: { height: '85%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, borderBottomWidth: 1 },
modalTitle: { fontSize: 20, fontWeight: '800' },
modalSubtitle: { fontSize: 14, fontWeight: '600', marginTop: 2 },
closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
sumarioCard: { padding: 16, borderRadius: 18, marginBottom: 15, borderWidth: 1 },
sumarioHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 },
sumarioData: { fontSize: 13, fontWeight: '800', marginLeft: 6 },
sumarioTexto: { fontSize: 15, lineHeight: 22, fontWeight: '500' },
emptyState: { alignItems: 'center', marginTop: 50 },
emptyText: { textAlign: 'center', marginTop: 15, fontSize: 14, fontWeight: '600' }
headerContainer: { paddingHorizontal: 24, paddingBottom: 20, paddingTop: 10 },
topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
btnBack: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
titleWrapper: { alignItems: 'center' },
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 },
searchWrapper: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 18, paddingHorizontal: 16, height: 54 },
inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' },
listContent: { paddingHorizontal: 24, paddingBottom: 40 },
turmaSection: { marginBottom: 30 },
turmaHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, paddingLeft: 4 },
turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 },
turmaTitle: { fontSize: 16, fontWeight: '800' },
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 24, marginBottom: 12, elevation: 4, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 1, shadowRadius: 12 },
avatarBox: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
avatarChar: { fontSize: 20, fontWeight: '900' },
alunoInfo: { flex: 1, marginLeft: 16 },
alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 },
alunoSub: { fontSize: 13, fontWeight: '500' },
goCircle: { width: 32, height: 32, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'flex-end' },
modalContent: { height: '88%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, elevation: 2 },
modalTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 },
closeBtn: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
modalScroll: { padding: 20, paddingBottom: 50 },
sumarioCard: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 1, shadowRadius: 6 },
sumarioHeaderRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 },
dateBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 },
dateText: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase' },
sumarioLine: { flex: 1, height: 1, backgroundColor: 'rgba(128,128,128,0.1)', marginLeft: 15 },
sumarioBody: { paddingLeft: 4 },
sumarioTexto: { fontSize: 15, lineHeight: 24, fontWeight: '500', letterSpacing: 0.2 },
emptyContainer: { alignItems: 'center', marginTop: 100 },
emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
emptyText: { fontSize: 16, fontWeight: '700' },
emptySub: { fontSize: 13, textAlign: 'center', marginTop: 8, opacity: 0.7, paddingHorizontal: 30 }
});

View File

@@ -34,7 +34,8 @@ const DetalhesEmpresa = memo(() => {
const params = useLocalSearchParams();
const insets = useSafeAreaInsets();
const azulPetroleo = '#2390a6';
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const empresaOriginal: Empresa = useMemo(() => {
if (!params.empresa) return {} as Empresa;
@@ -55,16 +56,17 @@ const DetalhesEmpresa = memo(() => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
vermelho: '#EF4444',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
vermelhoSuave: '#FFF5F5',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.4)',
overlay: 'rgba(26, 54, 93, 0.8)',
}), [isDarkMode]);
useEffect(() => {
@@ -119,7 +121,6 @@ const DetalhesEmpresa = memo(() => {
router.back();
} catch (e) {
setShowDeleteModal(false);
// Se tiver alunos ligados, vai dar merda ao apagar
alert('Não é possível apagar empresas com estágios ativos.');
} finally {
setLoading(false);
@@ -130,29 +131,27 @@ const DetalhesEmpresa = memo(() => {
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* FEEDBACK TOAST */}
{showSuccess && (
<View style={[styles.toast, { backgroundColor: cores.azul, top: insets.top + 10 }]}>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.toastText}>Alterações guardadas com sucesso!</Text>
<Text style={styles.toastText}>Dados atualizados!</Text>
</View>
)}
{/* MODAL DE APAGAR MODERNO */}
<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 }]}>
<Ionicons name="trash" size={32} color={cores.vermelho} />
<Ionicons name="warning" size={35} color={cores.vermelho} />
</View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Tens a certeza?</Text>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Entidade?</Text>
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>
Esta ação irá remover permanentemente a entidade <Text style={{fontWeight: '800'}}>{empresaLocal.nome}</Text>.
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}>
<TouchableOpacity
style={[styles.modalBtn, { backgroundColor: cores.fundo }]}
style={[styles.modalBtn, { backgroundColor: isDarkMode ? '#2D2D2D' : '#E2E8F0' }]}
onPress={() => setShowDeleteModal(false)}
>
<Text style={[styles.modalBtnTxt, { color: cores.texto }]}>Cancelar</Text>
@@ -162,7 +161,7 @@ const DetalhesEmpresa = memo(() => {
style={[styles.modalBtn, { backgroundColor: cores.vermelho }]}
onPress={confirmDelete}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.modalBtnTxt, { color: '#fff' }]}>Apagar</Text>}
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.modalBtnTxt, { color: '#fff' }]}>Confirmar</Text>}
</TouchableOpacity>
</View>
</View>
@@ -172,77 +171,84 @@ const DetalhesEmpresa = memo(() => {
<SafeAreaView style={styles.safe} edges={['top']}>
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnAction, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Detalhes da Entidade</Text>
<View style={{alignItems: 'center'}}>
<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>
</View>
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: editando ? cores.vermelho : cores.card, borderColor: editando ? cores.vermelho : cores.borda }]}
style={[styles.btnAction, { backgroundColor: editando ? cores.laranja : 'transparent', borderColor: editando ? cores.laranja : cores.borda }]}
onPress={() => {
if(editando) setEmpresaLocal({...empresaOriginal});
setEditando(!editando);
}}
>
<Ionicons name={editando ? "close" : "pencil"} size={20} color={editando ? "#fff" : cores.azul} />
<Ionicons name={editando ? "close" : "create-outline"} size={22} color={editando ? "#fff" : cores.azul} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 20 }]} showsVerticalScrollIndicator={false}>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 30 }]} showsVerticalScrollIndicator={false}>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.sectionHeader}>
<Ionicons name="business" size={18} color={cores.azul} />
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Gerais</Text>
<View style={[styles.sideLine, { backgroundColor: cores.azul }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Informação Institucional</Text>
</View>
<ModernField label="Nome" value={empresaLocal.nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} />
<ModernField label="Curso" value={empresaLocal.curso} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} />
<ModernField label="Morada" value={empresaLocal.morada} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline />
<ModernField label="Nome da Entidade" value={empresaLocal.nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} icon="business-outline" />
<ModernField label="Curso Associado" value={empresaLocal.curso} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} icon="school-outline" />
<ModernField label="Endereço Completo" value={empresaLocal.morada} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline icon="map-outline" />
</View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.sectionHeader}>
<Ionicons name="person" size={18} color={cores.azul} />
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Contacto Tutor</Text>
<View style={[styles.sideLine, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Gestão de Contacto</Text>
</View>
<ModernField label="Nome do Tutor" value={empresaLocal.tutor_nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} />
<ModernField label="Telefone" value={empresaLocal.tutor_telefone} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" />
<ModernField label="Tutor Responsável" value={empresaLocal.tutor_nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} icon="person-outline" />
<ModernField label="Contacto Telefónico" value={empresaLocal.tutor_telefone} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" icon="call-outline" />
</View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.sectionHeader}>
<Ionicons name="people" size={18} color={cores.azul} />
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Alunos Ativos</Text>
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.badgeTxt, { color: cores.azul }]}>{loadingAlunos ? '...' : alunos.length}</Text>
<View style={[styles.sideLine, { backgroundColor: cores.secundario }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Alunos em Estágio</Text>
<View style={[styles.countBadge, { backgroundColor: cores.azul }]}>
<Text style={styles.countBadgeTxt}>{loadingAlunos ? '-' : alunos.length}</Text>
</View>
</View>
{loadingAlunos ? <ActivityIndicator size="small" color={cores.azul} /> : alunos.length > 0 ? (
alunos.map((aluno, i) => (
<View key={aluno.id} style={[styles.alunoRow, i !== alunos.length - 1 && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View style={[styles.miniAvatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.miniAvatarTxt, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
<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>
))
) : <Text style={[styles.empty, { color: cores.secundario }]}>Sem alunos vinculados.</Text>}
) : <Text style={[styles.empty, { color: cores.secundario }]}>Nenhum aluno vinculado a esta entidade.</Text>}
</View>
<View style={{ marginTop: 5 }}>
<View style={styles.footerActions}>
{editando ? (
<TouchableOpacity style={[styles.btnSave, { backgroundColor: cores.azul }]} onPress={handleSave} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : (
<View style={styles.btnRow}><Ionicons name="cloud-done-outline" size={20} color="#fff" /><Text style={styles.btnSaveTxt}>Guardar Alterações</Text></View>
<View style={styles.btnRow}>
<Ionicons name="save-outline" size={20} color="#fff" />
<Text style={styles.btnSaveTxt}>Confirmar Alterações</Text>
</View>
)}
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.btnDel, { backgroundColor: cores.vermelhoSuave }]} onPress={() => setShowDeleteModal(true)}>
<Ionicons name="trash-outline" size={20} color={cores.vermelho} />
<Text style={[styles.btnDelTxt, { color: cores.vermelho }]}>Eliminar Entidade</Text>
<TouchableOpacity style={[styles.btnDel, { borderColor: cores.vermelho }]} onPress={() => setShowDeleteModal(true)}>
<Ionicons name="trash-bin-outline" size={18} color={cores.vermelho} />
<Text style={[styles.btnDelTxt, { color: cores.vermelho }]}>Remover Parceiro</Text>
</TouchableOpacity>
)}
</View>
@@ -252,14 +258,22 @@ const DetalhesEmpresa = memo(() => {
);
});
const ModernField = ({ label, value, editable, cores, ...props }: any) => (
const ModernField = ({ label, value, editable, cores, icon, ...props }: any) => (
<View style={styles.fieldContainer}>
<Text style={[styles.fieldLabel, { color: cores.secundario }]}>{label}</Text>
<View style={styles.labelRow}>
<Ionicons name={icon} size={12} 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 }]} value={value} selectionColor={cores.azul} {...props} />
<TextInput
style={[styles.input, { color: cores.texto, backgroundColor: cores.fundo, borderColor: cores.azul }]}
value={value}
selectionColor={cores.laranja}
{...props}
/>
) : (
<View style={[styles.readOnly, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.readOnlyTxt, { color: cores.texto }]}>{value || '---'}</Text>
<Text style={[styles.readOnlyTxt, { color: cores.texto }]}>{value || 'Não definido'}</Text>
</View>
)}
</View>
@@ -267,41 +281,43 @@ const ModernField = ({ label, value, editable, cores, ...props }: any) => (
const styles = StyleSheet.create({
safe: { flex: 1 },
toast: { position: 'absolute', left: 20, right: 20, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 16, gap: 10, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10 },
toastText: { color: '#fff', fontSize: 14, fontWeight: '800' },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
headerTitle: { fontSize: 18, fontWeight: '900' },
btnAction: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
scroll: { paddingHorizontal: 20, paddingTop: 10 },
card: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 },
sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginLeft: 8, flex: 1 },
fieldContainer: { marginBottom: 15 },
fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 5, marginLeft: 2 },
input: { fontSize: 14, fontWeight: '700', paddingHorizontal: 12, paddingVertical: 10, borderRadius: 12, borderWidth: 1.5 },
readOnly: { paddingHorizontal: 12, paddingVertical: 10, borderRadius: 12, borderWidth: 1 },
readOnlyTxt: { fontSize: 14, fontWeight: '700' },
badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6 },
badgeTxt: { fontSize: 12, fontWeight: '900' },
alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 },
miniAvatar: { width: 30, height: 30, borderRadius: 8, justifyContent: 'center', alignItems: 'center', marginRight: 10 },
miniAvatarTxt: { fontSize: 13, fontWeight: '900' },
alunoName: { fontSize: 14, fontWeight: '700' },
empty: { textAlign: 'center', fontSize: 12, fontWeight: '600', paddingVertical: 5 },
btnSave: { height: 54, borderRadius: 16, justifyContent: 'center', alignItems: 'center', marginTop: 10 },
btnRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
btnSaveTxt: { color: '#fff', fontSize: 15, fontWeight: '900', textTransform: 'uppercase' },
btnDel: { height: 54, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8 },
btnDelTxt: { fontSize: 15, fontWeight: '800' },
// Estilos do Modal
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
modalContent: { width: '100%', maxWidth: 340, borderRadius: 32, padding: 24, alignItems: 'center', borderWidth: 1, elevation: 20, shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 15 },
iconCircle: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
modalTitle: { fontSize: 22, fontWeight: '900', marginBottom: 10 },
modalSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 20, marginBottom: 25 },
modalButtons: { flexDirection: 'row', gap: 12, width: '100%' },
modalBtn: { flex: 1, height: 50, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
modalBtnTxt: { fontSize: 14, fontWeight: '800' }
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 },
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
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 },
sideLine: { width: 4, height: 18, borderRadius: 2 },
sectionTitle: { fontSize: 13, 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 },
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' },
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 },
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' },
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' }
});
export default DetalhesEmpresa;

View File

@@ -42,20 +42,21 @@ const ListaEmpresasProfessor = memo(() => {
const [refreshing, setRefreshing] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
// Estados do Formulário
const [form, setForm] = useState({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
const azulPetroleo = '#2390a6';
// Cores EPVC
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
branco: '#FFFFFF'
}), [isDarkMode]);
const fetchEmpresas = async () => {
@@ -90,7 +91,7 @@ const ListaEmpresasProfessor = memo(() => {
);
const grupos = filtradas.reduce((acc: { [key: string]: Empresa[] }, empresa) => {
const cursoKey = (empresa.curso || 'Outros').trim().toUpperCase();
const cursoKey = (empresa.curso || 'Geral').trim().toUpperCase();
if (!acc[cursoKey]) acc[cursoKey] = [];
acc[cursoKey].push(empresa);
return acc;
@@ -104,7 +105,7 @@ const ListaEmpresasProfessor = memo(() => {
const criarEmpresa = async () => {
if (!form.nome || !form.morada || !form.tutorNome || !form.tutorTelefone || !form.curso) {
Alert.alert('Atenção', 'Preenche todos os campos obrigatórios.');
Alert.alert('Atenção', 'Preencha todos os campos.');
return;
}
@@ -125,7 +126,7 @@ const ListaEmpresasProfessor = memo(() => {
setEmpresas(prev => [...prev, data![0]]);
setModalVisible(false);
setForm({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
Alert.alert('Sucesso', 'Entidade registada!');
Alert.alert('Sucesso', 'Empresa registada com sucesso!');
} catch (error: any) {
Alert.alert('Erro', error.message);
} finally {
@@ -139,37 +140,37 @@ const ListaEmpresasProfessor = memo(() => {
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER MODERNIZADO */}
{/* HEADER EPVC */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.backBtn, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Empresas</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
{empresas.length} entidades ativas
<Text style={[styles.headerTitle, { color: cores.texto }]}>Entidades</Text>
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>
{empresas.length} parcerias ativas
</Text>
</View>
<TouchableOpacity
style={[styles.addBtn, { backgroundColor: cores.azul }]}
style={[styles.addBtn, { backgroundColor: cores.laranja }]}
onPress={() => setModalVisible(true)}
>
<Ionicons name="add" size={28} color="#fff" />
<Ionicons name="add" size={30} color="#fff" />
</TouchableOpacity>
</View>
{/* SEARCH BAR */}
{/* PESQUISA */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.azul} />
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar entidade ou curso..."
placeholder="Procurar empresa ou curso..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
@@ -190,7 +191,8 @@ const ListaEmpresasProfessor = memo(() => {
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderSectionHeader={({ section: { title } }) => (
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{title}</Text>
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{title}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
)}
@@ -201,56 +203,58 @@ const ListaEmpresasProfessor = memo(() => {
onPress={() => router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', params: { empresa: JSON.stringify(item) } })}
>
<View style={[styles.empresaIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="business" size={22} color={cores.azul} />
<Ionicons name="business-outline" size={24} color={cores.azul} />
</View>
<View style={styles.empresaInfo}>
<Text style={[styles.empresaNome, { color: cores.texto }]}>{item.nome}</Text>
<View style={styles.tutorRow}>
<Ionicons name="person-circle-outline" size={14} color={cores.secundario} />
<Text style={[styles.tutorText, { color: cores.secundario }]}>{item.tutor_nome}</Text>
<Ionicons name="location-outline" size={12} color={cores.secundario} />
<Text style={[styles.tutorText, { color: cores.secundario }]} numberOfLines={1}>{item.morada}</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.4} />
<View style={[styles.arrowCircle, { borderColor: cores.borda }]}>
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
</View>
</TouchableOpacity>
)}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="business-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '600' }}>Sem resultados.</Text>
<Ionicons name="search-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 15, fontWeight: '700' }}>Nenhuma entidade encontrada.</Text>
</View>
)}
/>
)}
</SafeAreaView>
{/* MODAL DE CRIAÇÃO PREMIUM */}
{/* 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 }]}>Nova Empresa</Text>
<Text style={[styles.modalSub, { color: cores.secundario }]}>Preenche os detalhes da entidade</Text>
<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: 20 }}>
<ModernInput label="Nome da Entidade" icon="business" value={form.nome} onChangeText={(v:any)=>setForm({...form, nome:v})} cores={cores} />
<ModernInput label="Curso Principal" icon="school" value={form.curso} onChangeText={(v:any)=>setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI" />
<ModernInput label="Morada / Sede" icon="location" value={form.morada} onChangeText={(v:any)=>setForm({...form, morada:v})} cores={cores} />
<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} />
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
<ModernInput label="Tutor Responsável" icon="person" value={form.tutorNome} onChangeText={(v:any)=>setForm({...form, tutorNome:v})} cores={cores} />
<ModernInput label="Contacto Telefónico" icon="call" value={form.tutorTelefone} onChangeText={(v:any)=>setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} />
<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}>Registar Entidade</Text>
<Text style={styles.saveBtnText}>Confirmar Registo</Text>
</TouchableOpacity>
</ScrollView>
</View>
@@ -272,39 +276,41 @@ const ModernInput = ({ label, icon, cores, ...props }: any) => (
const styles = StyleSheet.create({
safe: { flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
headerSubtitle: { fontSize: 13, fontWeight: '600' },
addBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 5 },
searchSection: { paddingHorizontal: 20, marginVertical: 15 },
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 18, borderWidth: 1.5 },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, 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: 50, alignItems: 'center' },
emptyContainer: { marginTop: 80, alignItems: 'center' },
listPadding: { paddingHorizontal: 20 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 15 },
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 },
sectionLine: { flex: 1, height: 1, borderRadius: 1 },
empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 },
empresaIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
loadingCenter: { marginTop: 60, alignItems: 'center' },
emptyContainer: { marginTop: 100, alignItems: 'center' },
listPadding: { paddingHorizontal: 24 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 30, 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: 16, fontWeight: '800' },
tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 },
tutorText: { fontSize: 13, fontWeight: '600' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' },
modalContent: { borderTopLeftRadius: 35, borderTopRightRadius: 35, padding: 25, maxHeight: '85%' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 },
modalTitle: { fontSize: 22, fontWeight: '900' },
modalSub: { fontSize: 14, fontWeight: '600', marginTop: 2 },
closeBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' },
divider: { height: 1, backgroundColor: 'rgba(0,0,0,0.05)', marginVertical: 10 },
inputWrapper: { marginBottom: 18 },
inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 8, marginLeft: 5, letterSpacing: 1 },
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 16, borderWidth: 1.5 },
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: 58, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginTop: 20, elevation: 4 },
saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }
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 }
});
export default ListaEmpresasProfessor;

View File

@@ -37,7 +37,6 @@ export default function PerfilProfessor() {
const [loading, setLoading] = useState(true);
const [perfil, setPerfil] = useState<PerfilData | null>(null);
// --- SISTEMA DE AVISOS MODERNOS ---
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
const alertOpacity = useMemo(() => new Animated.Value(0), []);
@@ -50,24 +49,25 @@ export default function PerfilProfessor() {
]).start(() => setAlertConfig(null));
}, []);
const azulPetroleo = '#2390a6';
// Cores EPVC
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FFF5F5',
vermelho: '#EF4444',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
verde: '#38A169',
}), [isDarkMode]);
useEffect(() => {
carregarPerfil();
}, []);
useEffect(() => { carregarPerfil(); }, []);
async function carregarPerfil() {
try {
@@ -105,9 +105,9 @@ export default function PerfilProfessor() {
if (error) throw error;
setEditando(false);
showAlert('Perfil atualizado com sucesso!', 'success');
showAlert('Perfil atualizado!', 'success');
} catch (error: any) {
showAlert('Erro ao gravar dados no servidor.', 'error');
showAlert('Erro ao gravar dados.', 'error');
}
};
@@ -128,7 +128,6 @@ export default function PerfilProfessor() {
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
{/* BANNER DE AVISO */}
{alertConfig && (
<Animated.View style={[
styles.alertBar,
@@ -145,118 +144,81 @@ export default function PerfilProfessor() {
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* HEADER */}
<View style={styles.topBar}>
<TouchableOpacity style={[styles.backBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
<TouchableOpacity style={[styles.backBtn, { borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
<Text style={[styles.topTitle, { color: cores.texto }]}>Perfil</Text>
<TouchableOpacity
style={[styles.editBtn, { backgroundColor: editando ? cores.azul : cores.card }]}
style={[styles.editBtn, { backgroundColor: editando ? cores.laranja : cores.card, borderColor: editando ? cores.laranja : cores.borda }]}
onPress={() => editando ? guardarPerfil() : setEditando(true)}
>
<Ionicons name={editando ? "checkmark" : "create-outline"} size={20} color={editando ? "#fff" : cores.azul} />
<Ionicons name={editando ? "checkmark-sharp" : "create-outline"} size={20} color={editando ? "#fff" : cores.azul} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* AVATAR SECTION */}
<View style={styles.profileHeader}>
<View style={[styles.avatarContainer, { borderColor: cores.azulSuave }]}>
<View style={[styles.avatarBorder, { borderColor: cores.azul }]}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
</View>
{editando && (
<View style={[styles.editBadge, { backgroundColor: cores.laranja }]}>
<Ionicons name="camera" size={12} color="#fff" />
</View>
)}
</View>
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.userRole, { color: cores.secundario }]}>
{perfil?.curso || 'Professor / Tutor'}
</Text>
<View style={[styles.roleBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.userRole, { color: cores.azul }]}>{perfil?.curso || 'Professor'}</Text>
</View>
</View>
{/* INFO CARD */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<ModernInput
label="Nome Completo"
icon="person-outline"
value={perfil?.nome || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)}
cores={cores}
/>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<ModernInput label="Nome Completo" icon="person" value={perfil?.nome || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} />
<ModernInput
label="Área / Curso"
icon="book-outline"
value={perfil?.curso || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)}
cores={cores}
/>
<ModernInput label="Área de Formação" icon="school" value={perfil?.curso || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} />
<ModernInput
label="E-mail"
icon="mail-outline"
value={perfil?.email || ''}
editable={false}
cores={cores}
/>
<ModernInput label="Email Institucional" icon="mail" value={perfil?.email || ''} editable={false} cores={cores} />
<View style={styles.row}>
<View style={{ flex: 1, marginRight: 10 }}>
<ModernInput
label="Nº Escola"
icon="id-card-outline"
value={perfil?.n_escola || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)}
cores={cores}
/>
<ModernInput label="Nº Escola" icon="id-card" value={perfil?.n_escola || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} />
</View>
<View style={{ flex: 1.5 }}>
<ModernInput
label="Telefone"
icon="call-outline"
value={perfil?.telefone || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)}
keyboardType="phone-pad"
cores={cores}
/>
<ModernInput label="Telemóvel" icon="call" value={perfil?.telefone || ''} editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} />
</View>
</View>
</View>
{/* ACTIONS */}
<View style={styles.actionsContainer}>
<TouchableOpacity
style={[styles.menuItem, { backgroundColor: cores.card }]}
onPress={() => router.push('/Professor/redefenirsenha2')}
>
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.push('/Professor/redefenirsenha2')}>
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
</View>
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar palavra-passe</Text>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.menuItem, { backgroundColor: cores.card }]}
onPress={terminarSessao}
>
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={terminarSessao}>
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
<Ionicons name="power" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
{editando && (
<TouchableOpacity
style={[styles.cancelBtn]}
onPress={() => { setEditando(false); carregarPerfil(); }}
>
<Text style={[styles.cancelText, { color: cores.secundario }]}>Cancelar Edição</Text>
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setEditando(false); carregarPerfil(); }}>
<Text style={[styles.cancelText, { color: cores.laranja }]}>Cancelar Alterações</Text>
</TouchableOpacity>
)}
@@ -269,21 +231,9 @@ export default function PerfilProfessor() {
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: cores.fundo,
borderColor: editable ? cores.azul : cores.borda,
opacity: editable ? 1 : 0.8
}
]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 10 }} />
<TextInput
{...props}
editable={editable}
style={[styles.textInput, { color: cores.texto }]}
placeholderTextColor={cores.secundario}
/>
<View style={[styles.inputContainer, { backgroundColor: editable ? '#fff' : 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>
</View>
);
@@ -291,54 +241,31 @@ 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: 15,
flexDirection: 'row',
alignItems: 'center',
zIndex: 9999,
elevation: 10,
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 5
},
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 },
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 15
},
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 },
topTitle: { fontSize: 18, fontWeight: '800' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
profileHeader: { alignItems: 'center', marginVertical: 30 },
avatarContainer: { padding: 8, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' },
avatar: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center', elevation: 5 },
avatarLetter: { color: '#fff', fontSize: 32, fontWeight: '800' },
userName: { fontSize: 22, fontWeight: '800', marginTop: 15 },
userRole: { fontSize: 14, fontWeight: '500' },
card: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 },
inputWrapper: { marginBottom: 15 },
inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 },
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, height: 52, borderRadius: 16, borderWidth: 1.5 },
textInput: { flex: 1, fontSize: 15, fontWeight: '600' },
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', elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 },
avatarLetter: { color: '#fff', fontSize: 36, fontWeight: '900' },
editBadge: { position: 'absolute', bottom: 0, right: 0, width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 3, borderColor: '#fff' },
userName: { fontSize: 24, fontWeight: '900', marginTop: 15, letterSpacing: -0.5 },
roleBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 },
userRole: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
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 },
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
row: { flexDirection: 'row' },
actionsContainer: { gap: 12 },
menuItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 14,
borderRadius: 20,
elevation: 1
},
menuIcon: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
menuText: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '700' },
menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1 },
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' },
cancelText: { fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' }
cancelText: { fontSize: 14, fontWeight: '800', textDecorationLine: 'underline' }
});

View File

@@ -12,7 +12,6 @@ import {
TouchableOpacity,
View
} from 'react-native';
// Importação correta para controlo total de áreas seguras
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
@@ -22,19 +21,22 @@ const { width } = Dimensions.get('window');
export default function ProfessorMenu() {
const router = useRouter();
const { isDarkMode } = useTheme();
const insets = useSafeAreaInsets(); // Para ajustes finos se necessário
const insets = useSafeAreaInsets();
const [nome, setNome] = useState<string>('');
const [loading, setLoading] = useState(true);
const azulPetroleo = '#2390a6';
// Paleta EPVC Extraída da Imagem
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
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',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
}), [isDarkMode]);
@@ -61,64 +63,63 @@ export default function ProfessorMenu() {
}, []);
return (
// Aplicamos as edges para garantir que o fundo cobre tudo mas o conteúdo respeita os limites
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }} edges={['top', 'left', 'right']}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<ScrollView
contentContainerStyle={[
styles.content,
{ paddingBottom: insets.bottom + 20 } // Garante que o último card não cole no fundo
{ paddingBottom: insets.bottom + 20 }
]}
showsVerticalScrollIndicator={false}
>
{/* HEADER */}
{/* HEADER EPVC STYLE */}
<View style={styles.header}>
<View style={styles.headerRow}>
<View>
<Text style={[styles.welcome, { color: cores.textoSecundario }]}>Bem-vindo,</Text>
<Text style={[styles.welcome, { color: cores.laranja }]}>Olá, Professor</Text>
{loading ? (
<ActivityIndicator size="small" color={cores.azul} style={{ marginTop: 8, alignSelf: 'flex-start' }} />
) : (
<Text style={[styles.name, { color: cores.texto }]}>{nome || 'Professor'}</Text>
<Text style={[styles.name, { color: cores.texto }]}>{nome || 'Docente'}</Text>
)}
</View>
<TouchableOpacity
onPress={() => router.push('/Professor/PerfilProf')}
style={[styles.avatarMini, { backgroundColor: cores.azul }]}
>
<Text style={styles.avatarTxt}>{nome?.charAt(0).toUpperCase()}</Text>
<Text style={styles.avatarTxt}>{nome?.charAt(0).toUpperCase() || 'P'}</Text>
</TouchableOpacity>
</View>
<View style={[styles.infoBanner, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="stats-chart" size={18} color={cores.azul} />
<Text style={[styles.bannerTxt, { color: cores.textoSecundario }]}>Painel de Gestão de Estágios</Text>
<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 */}
{/* GRID DE MENU */}
<View style={styles.grid}>
<MenuCard icon="document-text" title="Sumários" subtitle="Registos Diários" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} />
<MenuCard icon="calendar" title="Presenças" subtitle="Controlo de Fluxo" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} />
<MenuCard icon="alert-circle" title="Faltas" subtitle="Justificações" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} />
<MenuCard icon="people" title="Alunos" subtitle="Gestão de Turmas" onPress={() => router.push('/Professor/Alunos/ListaAlunos')} cores={cores} />
<MenuCard icon="briefcase" title="Estágios" subtitle="Novos Projetos" onPress={() => router.push('/Professor/Alunos/Estagios')} cores={cores} />
<MenuCard icon="business" title="Empresas" subtitle="Parcerias Ativas" onPress={() => router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} />
<MenuCard icon="settings" title="Definições" subtitle="Sistema" onPress={() => router.push('/Professor/defenicoes2')} cores={cores} />
<MenuCard icon="person" title="Perfil" subtitle="Minha Conta" onPress={() => router.push('/Professor/PerfilProf')} cores={cores} />
<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} />
</View>
<View style={styles.footer}>
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>EPVC Gestão de Estágios</Text>
<View style={[styles.footerLine, { backgroundColor: cores.laranja }]} />
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>Estágios+ EPVC 2026</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
// O MenuCard permanece igual, apenas garantindo consistência
function MenuCard({ icon, title, subtitle, onPress, cores }: any) {
return (
<TouchableOpacity
@@ -127,45 +128,46 @@ function MenuCard({ icon, title, subtitle, onPress, cores }: any) {
activeOpacity={0.8}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={22} color={cores.azul} />
<Ionicons name={icon} size={24} color={cores.azul} />
</View>
<View>
<Text style={[styles.cardTitle, { color: cores.texto }]}>{title}</Text>
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]}>{subtitle}</Text>
</View>
<Ionicons name="chevron-forward" size={14} color={cores.textoSecundario} style={styles.arrow} />
<View style={[styles.arrowCircle, { backgroundColor: cores.laranja }]}>
<Ionicons name="chevron-forward" size={12} color="#FFF" />
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
content: { padding: 20 },
header: { marginBottom: 30 },
content: { padding: 24 },
header: { marginBottom: 35 },
headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
welcome: { fontSize: 13, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 },
name: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5, marginTop: 4 },
avatarMini: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
avatarTxt: { color: '#fff', fontSize: 20, fontWeight: '800' },
infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 16, borderWidth: 1 },
bannerTxt: { fontSize: 12, fontWeight: '700', marginLeft: 10, letterSpacing: 0.3 },
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 },
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
card: {
width: (width - 55) / 2, // Ajuste para melhor margem entre cards
borderRadius: 28,
padding: 18,
marginBottom: 15,
width: (width - 64) / 2,
borderRadius: 24,
padding: 20,
marginBottom: 16,
borderWidth: 1,
shadowColor: '#000',
shadowOpacity: 0.04,
shadowRadius: 10,
elevation: 2,
position: 'relative',
overflow: 'hidden'
shadowOpacity: 0.03,
shadowRadius: 15,
elevation: 3,
},
iconBox: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
cardTitle: { fontSize: 15, fontWeight: '800' },
cardSubtitle: { fontSize: 11, marginTop: 2, fontWeight: '600', opacity: 0.8 },
arrow: { position: 'absolute', top: 18, right: 15, opacity: 0.3 },
footer: { marginTop: 20, alignItems: 'center', opacity: 0.5 },
footerTxt: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }
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 },
footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 }
});

View File

@@ -23,7 +23,6 @@ const Definicoes = memo(() => {
const { isDarkMode, toggleTheme } = useTheme();
const [notificacoes, setNotificacoes] = useState(true);
// --- SISTEMA DE AVISOS MODERNOS ---
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
const alertOpacity = useMemo(() => new Animated.Value(0), []);
@@ -36,19 +35,22 @@ const Definicoes = memo(() => {
]).start(() => setAlertConfig(null));
}, []);
const azulPetroleo = '#2390a6';
// Cores EPVC
const azulEPVC = '#2390a6';
const laranjaEPVC = '#E38E00';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
secundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Branco puro do login
card: isDarkMode ? '#161618' : '#F8FAFC',
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
secundario: isDarkMode ? '#94A3B8' : '#718096',
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
vermelho: '#EF4444',
vermelhoSuave: 'rgba(239, 68, 68, 0.1)',
vermelhoSuave: '#FFF5F5',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
verde: '#10B981',
verde: '#38A169',
}), [isDarkMode]);
const handleLogout = async () => {
@@ -71,7 +73,6 @@ const Definicoes = memo(() => {
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* BANNER DE FEEDBACK */}
{alertConfig && (
<Animated.View style={[
styles.alertBar,
@@ -88,58 +89,58 @@ const Definicoes = memo(() => {
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER ESTILIZADO */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.btnVoltar, { borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{alignItems: 'center'}}>
<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>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* GRUPO: PERSONALIZAÇÃO */}
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Personalização</Text>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Preferências</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
<View style={[styles.item, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="notifications" size={20} color={cores.azul} />
<Ionicons name="notifications-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações Push</Text>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações</Text>
<Switch
value={notificacoes}
onValueChange={(v) => {
setNotificacoes(v);
showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info");
showAlert(v ? "Notificações ligadas" : "Notificações desligadas", "info");
}}
trackColor={{ false: '#CBD5E1', true: cores.azul }}
trackColor={{ false: '#CBD5E1', true: cores.laranja }}
thumbColor="#FFFFFF"
/>
</View>
<View style={styles.item}>
<View style={[styles.iconContainer, { backgroundColor: isDarkMode ? '#334155' : '#F1F5F9' }]}>
<Ionicons name={isDarkMode ? "moon" : "sunny"} size={20} color={isDarkMode ? '#FACC15' : '#F59E0B'} />
<Ionicons name={isDarkMode ? "moon-outline" : "sunny-outline"} size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Interface Escura</Text>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Modo Escuro</Text>
<Switch
value={isDarkMode}
onValueChange={toggleTheme}
trackColor={{ false: '#CBD5E1', true: cores.azul }}
trackColor={{ false: '#CBD5E1', true: cores.laranja }}
thumbColor="#FFFFFF"
/>
</View>
</View>
{/* GRUPO: CONTACTOS EPVC */}
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 25 }]}>Escola Profissional</Text>
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 30 }]}>Escola Profissional</Text>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<SettingLink
icon="business"
icon="at-outline"
label="Direção Geral"
subLabel="epvc@epvc.pt"
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
@@ -147,15 +148,15 @@ const Definicoes = memo(() => {
showBorder
/>
<SettingLink
icon="mail"
icon="mail-unread-outline"
label="Secretaria"
subLabel="secretaria@epvc.pt"
subLabel="Atendimento ao Docente"
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
cores={cores}
showBorder
/>
<SettingLink
icon="call"
icon="call-outline"
label="Linha Direta"
subLabel="252 641 805"
onPress={() => abrirURL('tel:252641805')}
@@ -163,28 +164,28 @@ const Definicoes = memo(() => {
/>
</View>
{/* GRUPO: SEGURANÇA & INFO */}
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 25 }]}>
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 30 }]}>
<View style={[styles.item, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
<Ionicons name="code-working-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Versão Estável</Text>
<Text style={[styles.versionBadge, { color: cores.secundario }]}>v2.6.17</Text>
<Text style={[styles.itemTexto, { color: cores.texto }]}>App Version</Text>
<Text style={[styles.versionBadge, { color: cores.azul, backgroundColor: cores.azulSuave }]}>2.6.17</Text>
</View>
<TouchableOpacity style={styles.item} onPress={handleLogout}>
<View style={[styles.iconContainer, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out" size={20} color={cores.vermelho} />
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '800' }]}>Terminar Sessão</Text>
<Ionicons name="chevron-forward" size={18} color={cores.vermelho} opacity={0.5} />
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '900' }]}>Terminar Sessão</Text>
<Ionicons name="arrow-forward-circle-outline" size={20} color={cores.vermelho} opacity={0.5} />
</TouchableOpacity>
</View>
<View style={styles.footer}>
<Text style={[styles.footerText, { color: cores.secundario }]}>Desenvolvido para PAP 2026</Text>
<Text style={[styles.footerText, { color: cores.azul, fontWeight: '800', marginTop: 4 }]}>EPVC</Text>
<View style={[styles.footerLine, { backgroundColor: cores.laranja }]} />
<Text style={[styles.footerText, { color: cores.secundario }]}>Estágios+ Vila do Conde</Text>
<Text style={[styles.footerText, { color: cores.azul, marginTop: 4, fontSize: 13 }]}>EPVC</Text>
</View>
</ScrollView>
@@ -193,7 +194,6 @@ const Definicoes = memo(() => {
);
});
// Componente Auxiliar para Links
const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => (
<TouchableOpacity
style={[styles.item, showBorder && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
@@ -204,28 +204,33 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any)
</View>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>{label}</Text>
<Text style={{ color: cores.secundario, fontSize: 12, marginTop: 2 }}>{subLabel}</Text>
<Text style={{ color: cores.secundario, fontSize: 12, fontWeight: '600' }}>{subLabel}</Text>
</View>
<View style={[styles.arrowCircle, { borderColor: cores.borda }]}>
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.3} />
</TouchableOpacity>
);
const styles = StyleSheet.create({
safe: { flex: 1 },
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 8 },
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1, fontSize: 14 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
btnVoltar: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 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 },
btnVoltar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
scrollContent: { paddingHorizontal: 20, paddingBottom: 50 },
sectionLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 1.2 },
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 15, elevation: 2 },
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16 },
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 },
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' },
versionBadge: { fontSize: 13, fontWeight: 'bold', backgroundColor: 'rgba(0,0,0,0.05)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
footer: { alignItems: 'center', marginTop: 40, opacity: 0.6 },
footerText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 }
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' },
footer: { alignItems: 'center', marginTop: 50 },
footerLine: { width: 30, height: 3, borderRadius: 2, marginBottom: 15 },
footerText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }
});
export default Definicoes;