ATUALIZACOES

This commit is contained in:
2026-03-17 17:21:21 +00:00
parent 3404c0044d
commit ff2b3fd8e7
17 changed files with 2058 additions and 1694 deletions

View File

@@ -1,21 +1,31 @@
// 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 * as DocumentPicker from 'expo-document-picker';
// ALTERADO: Importação da API Legacy para evitar o erro de depreciação
import { decode } from 'base64-arraybuffer';
import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system/legacy';
import * as Location from 'expo-location';
import { useRouter } from 'expo-router';
import { memo, useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator, Alert, Linking, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View
ActivityIndicator,
Animated,
Linking,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { Calendar, LocaleConfig } from 'react-native-calendars';
import { useTheme } from '../../themecontext';
import { supabase } from '../lib/supabase';
// Configuração PT
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'],
@@ -24,42 +34,62 @@ LocaleConfig.locales['pt'] = {
};
LocaleConfig.defaultLocale = 'pt';
const getFeriadosMap = (ano: number) => {
return {
[`${ano}-01-01`]: "Ano Novo",
[`${ano}-04-25`]: "Dia da Liberdade",
[`${ano}-05-01`]: "Dia do Trabalhador",
[`${ano}-06-10`]: "Dia de Portugal",
[`${ano}-06-24`]: "São João (Vila do Conde)",
[`${ano}-08-15`]: "Assunção de Nª Senhora",
[`${ano}-10-05`]: "Implantação da República",
[`${ano}-11-01`]: "Todos os Santos",
[`${ano}-12-01`]: "Restauração da Independência",
[`${ano}-12-08`]: "Imaculada Conceição",
[`${ano}-12-25`]: "Natal"
};
};
const getFeriadosMap = (ano: number) => ({
[`${ano}-01-01`]: "Ano Novo", [`${ano}-04-25`]: "Dia da Liberdade",
[`${ano}-05-01`]: "Dia do Trabalhador", [`${ano}-06-10`]: "Dia de Portugal",
[`${ano}-06-24`]: "São João (Vila do Conde)", [`${ano}-08-15`]: "Assunção de Nª Senhora",
[`${ano}-10-05`]: "Implantação da República", [`${ano}-11-01`]: "Todos os Santos",
[`${ano}-12-01`]: "Restauração da Independência", [`${ano}-12-08`]: "Imaculada Conceição",
[`${ano}-12-25`]: "Natal"
});
const AlunoHome = memo(() => {
const { isDarkMode } = useTheme();
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 [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 [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';
const themeStyles = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: azulPetroleo,
vermelho: '#EF4444',
verde: '#10B981',
}), [isDarkMode]);
useFocusEffect(
useCallback(() => {
carregarConfigLocal();
@@ -79,151 +109,73 @@ const AlunoHome = memo(() => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data, error } = await supabase
.from('presencas')
.select('data, estado, sumario, justificacao_url')
.eq('aluno_id', user.id);
const { data, error } = await supabase.from('presencas').select('*').eq('aluno_id', user.id);
if (error) throw error;
const objPresencas: Record<string, boolean> = {};
const objFaltas: Record<string, boolean> = {};
const objSumarios: Record<string, string> = {};
const objUrls: Record<string, string> = {};
const p: any = {}, f: any = {}, s: any = {}, u: any = {};
data?.forEach(item => {
if (item.estado === 'presente') {
objPresencas[item.data] = true;
if (item.sumario) objSumarios[item.data] = item.sumario;
} else if (item.estado === 'faltou') {
objFaltas[item.data] = true;
if (item.justificacao_url) objUrls[item.data] = item.justificacao_url;
}
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(objPresencas);
setFaltas(objFaltas);
setSumarios(objSumarios);
setUrlsJustificacao(objUrls);
setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u);
} catch (error: any) {
console.error("Erro na BD:", error.message);
console.error(error);
} finally {
setIsLoadingDB(false);
}
};
const themeStyles = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: '#3B82F6',
vermelho: '#EF4444',
verde: '#10B981',
}), [isDarkMode]);
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
const listaFeriados = useMemo(() => Object.keys(feriadosMap), [feriadosMap]);
const infoData = useMemo(() => {
const data = new Date(selectedDate);
const diaSemana = data.getDay();
const ehFimDeSemana = diaSemana === 0 || diaSemana === 6;
const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
const ehHoje = selectedDate === hojeStr;
const ehFuturo = selectedDate > hojeStr;
const nomeFeriado = feriadosMap[selectedDate];
const fora = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
return {
valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado,
podeMarcarPresenca: ehHoje && !foraDoIntervalo && !nomeFeriado,
ehFuturo,
nomeFeriado
valida: diaSemana !== 0 && diaSemana !== 6 && !fora && !nomeFeriado,
podeMarcar: selectedDate === hojeStr && !fora && !nomeFeriado,
nomeFeriado
};
}, [selectedDate, configEstagio, hojeStr, feriadosMap]);
const diasMarcados: any = useMemo(() => {
const marcacoes: any = {};
listaFeriados.forEach(d => { marcacoes[d] = { marked: true, dotColor: '#3B82F6' }; });
Object.keys(presencas).forEach((d) => {
const temSumario = sumarios[d] && sumarios[d].trim().length > 0;
marcacoes[d] = { marked: true, dotColor: temSumario ? '#10B981' : '#F59E0B' };
});
Object.keys(faltas).forEach((d) => {
marcacoes[d] = { marked: true, dotColor: urlsJustificacao[d] ? '#64748B' : '#EF4444' };
});
marcacoes[selectedDate] = { ...(marcacoes[selectedDate] || {}), selected: true, selectedColor: '#3B82F6' };
return marcacoes;
}, [presencas, faltas, sumarios, urlsJustificacao, selectedDate, listaFeriados]);
const handlePresenca = async () => {
if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "Data inválida.");
if (!infoData.podeMarcar) return showAlert("Não podes marcar presença nesta data.", "error");
setIsLocating(true);
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') throw new Error("Permissão de localização necessária.");
const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
const { latitude, longitude } = location.coords;
if (status !== 'granted') throw new Error("Sem permissão de GPS.");
const loc = await Location.getCurrentPositionAsync({});
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Não autenticado.");
const { error } = await supabase.from('presencas').upsert({
aluno_id: user.id,
data: selectedDate,
estado: 'presente',
lat: latitude,
lng: longitude
aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude
});
if (error) throw error;
Alert.alert("Sucesso", "Presença registada!");
fetchDadosSupabase();
showAlert("Presença registada com sucesso!", "success");
fetchDadosSupabase();
} catch (e: any) {
Alert.alert("Erro", "Falha ao registar presença.");
} finally {
setIsLocating(false);
}
showAlert(e.message, "error");
} finally { setIsLocating(false); }
};
const handleFalta = async () => {
if (!infoData.valida) return Alert.alert("Bloqueado", "Data inválida.");
if (!infoData.valida) return showAlert("Data inválida para registar falta.", "error");
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase.from('presencas').upsert({
aluno_id: user.id,
data: selectedDate,
estado: 'faltou'
});
if (error) throw error;
await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' });
showAlert("Falta registada.", "info");
fetchDadosSupabase();
} catch (e) { Alert.alert("Erro", "Falha ao registar falta."); }
} catch (e) { showAlert("Erro ao registar falta.", "error"); }
};
const guardarSumario = async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase.from('presencas').update({ sumario: sumarios[selectedDate] })
.match({ aluno_id: user.id, data: selectedDate });
if (error) throw error;
await supabase.from('presencas').update({ sumario: sumarios[selectedDate] }).match({ aluno_id: user?.id, data: selectedDate });
setEditandoSumario(false);
Alert.alert("Sucesso", "Sumário guardado!");
showAlert("Sumário atualizado!", "success");
fetchDadosSupabase();
} catch (e) { Alert.alert("Erro", "Falha ao guardar sumário."); }
};
const escolherPDF = async () => {
const result = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
if (!result.canceled) setPdf(result.assets[0]);
} catch (e) { showAlert("Erro ao guardar sumário.", "error"); }
};
const enviarJustificacao = async () => {
@@ -231,64 +183,40 @@ const AlunoHome = memo(() => {
setIsUploading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Utilizador não autenticado");
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 fileExt = pdf.name.split('.').pop();
const fileName = `${user.id}/${selectedDate}_${Date.now()}.${fileExt}`;
// SOLUÇÃO: Agora a API legacy já permite o readAsStringAsync sem avisos
const base64 = await FileSystem.readAsStringAsync(pdf.uri, {
encoding: 'base64'
});
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
const { error: uploadError } = await supabase.storage
.from('justificacoes')
.upload(fileName, decode(base64), {
contentType: 'application/pdf',
upsert: true
});
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage
.from('justificacoes')
.getPublicUrl(fileName);
const { error: dbError } = await supabase
.from('presencas')
.update({ justificacao_url: publicUrl })
.match({ aluno_id: user.id, data: selectedDate });
if (dbError) throw dbError;
Alert.alert("Sucesso", "Justificação enviada!");
showAlert("Documento enviado!", "success");
setPdf(null);
fetchDadosSupabase();
} catch (e: any) {
console.error(e);
Alert.alert("Erro", "Se a net falhar, vai dar merda: " + e.message);
} finally {
setIsUploading(false);
}
} catch (e: any) { showAlert(e.message, "error"); }
finally { setIsUploading(false); }
};
const visualizarDocumento = async (url: string) => {
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
}
} catch (e) {
Alert.alert("Erro", "Não foi possível abrir o documento.");
}
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 */}
{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" />
<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} />
@@ -301,9 +229,9 @@ const AlunoHome = memo(() => {
<View style={styles.botoesLinha}>
<TouchableOpacity
style={[styles.btn, { backgroundColor: themeStyles.azul }, (!infoData.podeMarcarPresenca || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
onPress={handlePresenca}
disabled={!infoData.podeMarcarPresenca || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
disabled={!infoData.podeMarcar || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
>
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
</TouchableOpacity>
@@ -312,84 +240,67 @@ const AlunoHome = memo(() => {
onPress={handleFalta}
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
>
<Text style={styles.txtBtn}>{infoData.ehFuturo ? "Vou Faltar" : "Faltei"}</Text>
<Text style={styles.txtBtn}>Faltei</Text>
</TouchableOpacity>
</View>
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
{isLoadingDB && (
<View style={styles.loaderOverlay}>
<ActivityIndicator size="large" color={themeStyles.azul} />
</View>
)}
{isLoadingDB && <View style={styles.loaderOverlay}><ActivityIndicator size="large" color={azulPetroleo} /></View>}
<Calendar
key={isDarkMode ? 'dark' : 'light'}
theme={{
calendarBackground: themeStyles.card,
dayTextColor: themeStyles.texto,
monthTextColor: themeStyles.texto,
todayTextColor: themeStyles.azul,
arrowColor: themeStyles.azul,
selectedDayBackgroundColor: themeStyles.azul
calendarBackground: themeStyles.card,
dayTextColor: themeStyles.texto,
monthTextColor: themeStyles.texto,
todayTextColor: azulPetroleo,
arrowColor: azulPetroleo,
selectedDayBackgroundColor: azulPetroleo
}}
markedDates={{
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
...Object.keys(presencas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#10B981' } }), {}),
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#EF4444' } }), {}),
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
}}
markedDates={diasMarcados}
onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }}
/>
</View>
{infoData.nomeFeriado && (
<View style={[styles.cardFeriado, { backgroundColor: themeStyles.azul + '15' }]}>
<Ionicons name="gift-outline" size={18} color={themeStyles.azul} />
<Text style={[styles.txtFeriado, { color: themeStyles.azul }]}>{infoData.nomeFeriado}</Text>
</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 do Dia</Text>
{!editandoSumario && (
<TouchableOpacity onPress={() => setEditandoSumario(true)}>
<Ionicons name="create-outline" size={22} color={themeStyles.azul} />
</TouchableOpacity>
)}
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
{!editandoSumario && <TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>}
</View>
<TextInput
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto, opacity: editandoSumario ? 1 : 0.7 }]}
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto }]}
multiline editable={editandoSumario}
value={sumarios[selectedDate] || ''}
onChangeText={(txt) => setSumarios({ ...sumarios, [selectedDate]: txt })}
placeholder="Descreve as tuas tarefas..."
placeholderTextColor={themeStyles.textoSecundario}
value={sumarios[selectedDate]}
onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})}
placeholder="O que fizeste hoje?"
placeholderTextColor="#94A3B8"
/>
{editandoSumario && (
<TouchableOpacity style={[styles.btnAcao, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}>
<Text style={styles.txtBtn}>Guardar Sumário</Text>
</TouchableOpacity>
)}
{editandoSumario && <TouchableOpacity style={[styles.btnAcao, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar</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 de Falta</Text>
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificação</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 Justificação (PDF)</Text>
<Text style={[styles.txtBtn, { marginLeft: 8 }]}>Ver PDF</Text>
</TouchableOpacity>
) : (
<View>
<TouchableOpacity style={[styles.btnAnexar, { borderColor: themeStyles.borda }]} onPress={escolherPDF}>
<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>
<TouchableOpacity
style={[styles.btnAcao, { backgroundColor: themeStyles.verde }, (!pdf || isUploading) && styles.disabled]}
onPress={enviarJustificacao}
disabled={!pdf || isUploading}
>
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Enviar para o Servidor</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>
)}
</View>
@@ -401,24 +312,24 @@ 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' },
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
btn: { padding: 16, borderRadius: 16, width: '48%', alignItems: 'center', elevation: 2 },
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' },
disabled: { opacity: 0.4 },
txtBtn: { color: '#fff', fontWeight: 'bold', fontSize: 14 },
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 },
cardFeriado: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 15, padding: 12, borderRadius: 12 },
txtFeriado: { fontWeight: '700', marginLeft: 8 },
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: 120, textAlignVertical: 'top', fontSize: 15 }
input: { borderWidth: 1, borderRadius: 12, padding: 15, height: 100, textAlignVertical: 'top' }
});
export default AlunoHome;

View File

@@ -1,3 +1,4 @@
// app/Professor/Alunos/DetalhesAluno.tsx
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
@@ -10,7 +11,7 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
@@ -36,30 +37,27 @@ const DetalhesAlunos = memo(() => {
const router = useRouter();
const params = useLocalSearchParams();
const { isDarkMode } = useTheme();
const insets = useSafeAreaInsets();
const azulPetroleo = '#2390a6';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
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)',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
}), [isDarkMode]);
const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null;
const [aluno, setAluno] = useState<AlunoEstado | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (alunoId) fetchAluno();
}, [alunoId]);
const fetchAluno = async () => {
try {
setLoading(true);
const { data, error } = await supabase
.from('alunos')
.select(`
@@ -74,24 +72,28 @@ const DetalhesAlunos = memo(() => {
.single();
if (error) throw error;
if (data) {
const d = data as any;
const perfil = d.profiles;
// 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?.empresas;
const empresa = estagio && (Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas);
let listaHorarios: string[] = [];
if (estagio?.id) {
const { data: hData } = await supabase
const { data: hData, error: hError } = await supabase
.from('horarios_estagio')
.select('hora_inicio, hora_fim')
.eq('estagio_id', estagio.id);
if (hData) {
listaHorarios = hData.map(h =>
`${h.hora_inicio.substring(0,5)} até às ${h.hora_fim.substring(0,5)}`
);
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}`;
});
}
}
@@ -109,17 +111,21 @@ const DetalhesAlunos = memo(() => {
tutor_tel: empresa?.tutor_telefone || '-',
data_inicio: estagio?.data_inicio || '-',
data_fim: estagio?.data_fim || '-',
horas_diarias: estagio?.horas_diarias || '0h',
horarios_detalhados: listaHorarios || [], // Garantia de array vazio
horas_diarias: estagio?.horas_diarias ? `${estagio.horas_diarias}h` : '0h',
horarios_detalhados: listaHorarios,
});
}
} catch (err: any) {
console.log('Erro:', err.message);
console.log('Erro ao carregar aluno:', err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (alunoId) fetchAluno();
}, [alunoId]);
if (loading) {
return (
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
@@ -128,115 +134,128 @@ const DetalhesAlunos = memo(() => {
);
}
// Verificação de segurança extra para o objeto aluno
if (!aluno) {
return (
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
<Text style={{ color: cores.texto }}>Dados indisponíveis</Text>
</View>
);
}
return (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]} edges={['top']}>
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: cores.card }]}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
<View style={{ width: 40 }} />
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<SafeAreaView style={styles.safe} edges={['top']}>
<View style={styles.profileSection}>
<View style={[styles.mainAvatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarLetter, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<Text style={[styles.mainName, { color: cores.texto }]}>{aluno.nome}</Text>
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.badgeText, { color: cores.azul }]}>{aluno.turma_curso}</Text>
</View>
</View>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Pessoais</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<InfoRow icon="school-outline" label="Nº Escola" valor={aluno.n_escola} cores={cores} />
<InfoRow icon="mail-outline" label="Email" valor={aluno.email} cores={cores} />
<InfoRow icon="call-outline" label="Telefone" valor={aluno.telefone} cores={cores} />
<InfoRow icon="location-outline" label="Residência" valor={aluno.residencia} cores={cores} ultimo />
</View>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Informação de Estágio</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<InfoRow icon="business-outline" label="Empresa" valor={aluno.empresa_nome} cores={cores} />
<InfoRow icon="person-outline" label="Tutor / Responsável" valor={aluno.tutor_nome} cores={cores} />
{/* HEADER */}
<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>
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="time-outline" size={18} color={cores.azul} />
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: cores.secundario }]}>Horários Registados</Text>
{/* O USO DE OPTIONAL CHAINING AQUI EVITA O ERRO */}
{(aluno.horarios_detalhados?.length ?? 0) > 0 ? (
aluno.horarios_detalhados.map((h, index) => (
<Text key={index} style={[styles.infoValor, { color: cores.texto, marginBottom: 2 }]}>{h}</Text>
))
) : (
<Text style={[styles.infoValor, { color: cores.texto }]}>Sem horários definidos</Text>
)}
</View>
</View>
<View style={{ flexDirection: 'row', borderTopWidth: 1, borderTopColor: cores.borda }}>
<View style={{ flex: 1 }}>
<InfoRow icon="calendar-outline" label="Início" valor={aluno.data_inicio} cores={cores} ultimo />
</View>
<View style={{ flex: 1 }}>
<InfoRow icon="calendar-outline" label="Previsão Fim" valor={aluno.data_fim} cores={cores} ultimo />
</View>
<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>
</SafeAreaView>
<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>
</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>
</View>
</View>
{/* DADOS PESSOAIS */}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Pessoais</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>
{/* 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.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>
<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>
</View>
</ScrollView>
</SafeAreaView>
</View>
);
});
const InfoRow = ({ icon, label, valor, cores, ultimo }: any) => (
<View style={[styles.infoRow, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={18} color={cores.azul} />
</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} />
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: cores.secundario }]}>{label}</Text>
<Text style={[styles.infoValor, { color: cores.texto }]} numberOfLines={1}>{valor}</Text>
<Text style={[styles.rowLabel, { color: cores.secundario }]}>{label}</Text>
<Text style={[styles.rowValue, { color: cores.texto }]}>{value}</Text>
</View>
</View>
);
export default DetalhesAlunos;
const styles = StyleSheet.create({
safe: { flex: 1 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
backBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
headerTitle: { fontSize: 18, fontWeight: '800' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
profileSection: { alignItems: 'center', marginBottom: 25 },
mainAvatar: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
avatarLetter: { fontSize: 28, fontWeight: '800' },
mainName: { fontSize: 22, fontWeight: '800', textAlign: 'center' },
badge: { marginTop: 6, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 20 },
badgeText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase' },
sectionTitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginTop: 25, marginBottom: 10, marginLeft: 5 },
card: { borderRadius: 20, padding: 10, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
infoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 10 },
iconBox: { width: 36, height: 36, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
infoLabel: { fontSize: 10, fontWeight: '600', textTransform: 'uppercase', marginBottom: 2 },
infoValor: { fontSize: 15, fontWeight: '700' },
});
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 },
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' }
});
export default DetalhesAlunos;

View File

@@ -2,7 +2,6 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Modal,
ScrollView,
@@ -13,21 +12,13 @@ import {
TouchableOpacity,
View
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
// --- Interfaces ---
interface Horario {
id?: number;
periodo: string;
hora_inicio: string;
hora_fim: string;
}
interface Aluno { id: string; nome: string; turma_curso: string; ano: number; }
interface Empresa { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; }
interface Estagio {
id: string;
aluno_id: string;
@@ -42,54 +33,48 @@ interface Estagio {
export default function Estagios() {
const router = useRouter();
const { isDarkMode } = useTheme();
const insets = useSafeAreaInsets();
const azulPetroleo = '#2390a6';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
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)',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
vermelho: '#EF4444',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.5)',
}), [isDarkMode]);
// Estados de Dados
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 [passo, setPasso] = useState(1);
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);
const [editandoEstagio, setEditandoEstagio] = useState<Estagio | null>(null);
const [dataInicio, setDataInicio] = useState('');
const [dataFim, setDataFim] = useState('');
const [horarios, setHorarios] = useState<Horario[]>([]);
const [searchMain, setSearchMain] = useState('');
const [searchAluno, setSearchAluno] = useState('');
const [searchEmpresa, setSearchEmpresa] = useState('');
const totalHorasDiarias = useMemo(() => {
let totalMinutos = 0;
horarios.forEach(h => {
const [hIni, mIni] = h.hora_inicio.split(':').map(Number);
const [hFim, mFim] = h.hora_fim.split(':').map(Number);
if (!isNaN(hIni) && !isNaN(hFim)) {
const inicio = hIni * 60 + (mIni || 0);
const fim = hFim * 60 + (mFim || 0);
if (fim > inicio) totalMinutos += (fim - inicio);
}
});
const h = Math.floor(totalMinutos / 60);
const m = totalMinutos % 60;
return m > 0 ? `${h}h${m}m` : `${h}h`;
}, [horarios]);
// 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 () => {
@@ -106,44 +91,52 @@ export default function Estagios() {
} catch (e) { console.error(e); } finally { setLoading(false); }
};
const carregarHorarios = async (estagioId: string) => {
const { data } = await supabase.from('horarios_estagio').select('*').eq('estagio_id', estagioId);
if (data) setHorarios(data);
const totalHorasDiarias = useMemo(() => {
const calcularMinutos = (ini: string, fim: string) => {
if (!ini || !fim) return 0;
const [hI, mI] = ini.split(':').map(Number);
const [hF, mF] = fim.split(':').map(Number);
const totalI = (hI || 0) * 60 + (mI || 0);
const totalF = (hF || 0) * 60 + (mF || 0);
return totalF > totalI ? totalF - totalI : 0;
};
const totalMinutos = calcularMinutos(hManhaIni, hManhaFim) + calcularMinutos(hTardeIni, hTardeFim);
const h = Math.floor(totalMinutos / 60);
const m = totalMinutos % 60;
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 handleFecharModal = () => {
setModalVisible(false);
setEditandoEstagio(null);
setAlunoSelecionado(null);
setEmpresaSelecionada(null);
setHorarios([]);
setDataInicio('');
setDataFim('');
setPasso(1);
};
const eliminarEstagio = (id: string) => {
Alert.alert("Eliminar Estágio", "Deseja remover este estágio?", [
{ text: "Cancelar", style: "cancel" },
{ text: "Eliminar", style: "destructive", onPress: async () => {
await supabase.from('estagios').delete().eq('id', id);
fetchDados();
}}
]);
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();
}
setLoading(false);
};
const salvarEstagio = async () => {
setLoading(true);
const { data: { user } } = await supabase.auth.getUser();
if (empresaSelecionada) {
await supabase.from('empresas').update({
morada: empresaSelecionada.morada,
tutor_nome: empresaSelecionada.tutor_nome,
tutor_telefone: empresaSelecionada.tutor_telefone
}).eq('id', empresaSelecionada.id);
}
const payloadEstagio = {
const payload = {
aluno_id: alunoSelecionado?.id,
empresa_id: empresaSelecionada?.id,
professor_id: user?.id,
@@ -154,186 +147,156 @@ export default function Estagios() {
};
const { data: estData, error: errE } = editandoEstagio
? await supabase.from('estagios').update(payloadEstagio).eq('id', editandoEstagio.id).select().single()
: await supabase.from('estagios').insert([payloadEstagio]).select().single();
? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single()
: await supabase.from('estagios').insert([payload]).select().single();
if (errE) return Alert.alert("Erro", errE.message);
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);
if (horarios.length > 0) {
const payloadH = horarios.map(h => ({
estagio_id: currentId,
periodo: h.periodo,
hora_inicio: h.hora_inicio,
hora_fim: h.hora_fim
}));
await supabase.from('horarios_estagio').insert(payloadH);
}
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 alunosAgrupados = useMemo(() => {
const groups: Record<string, Aluno[]> = {};
alunos.filter(a => a.nome.toLowerCase().includes(searchAluno.toLowerCase())).forEach(a => {
const k = a.turma_curso ? `${a.ano}º ${a.turma_curso}` : 'Sem Turma';
if (!groups[k]) groups[k] = [];
groups[k].push(a);
});
return groups;
}, [alunos, searchAluno]);
const handleFecharModal = () => {
setModalVisible(false);
setEditandoEstagio(null);
setAlunoSelecionado(null);
setEmpresaSelecionada(null);
setDataInicio(''); setDataFim('');
setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
setPasso(1);
};
// CORREÇÃO DO AGRUPAMENTO DE EMPRESAS PARA EVITAR DUPLICADOS
const empresasAgrupadas = useMemo(() => {
const groups: Record<string, Empresa[]> = {};
empresas
.filter(e => e.nome.toLowerCase().includes(searchEmpresa.toLowerCase()))
.forEach(e => {
const k = e.curso ? e.curso.trim() : 'Geral';
if (!groups[k]) groups[k] = [];
groups[k].push(e);
});
return groups;
}, [empresas, searchEmpresa]);
const estagiosAgrupados = useMemo(() => {
// --- 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';
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));
return Object.keys(groups).map(titulo => ({ titulo, dados: groups[titulo] })).sort((a, b) => b.titulo.localeCompare(a.titulo));
}, [estagios, searchMain]);
if (loading) return <View style={{flex:1, justifyContent:'center', backgroundColor:cores.fundo}}><ActivityIndicator size="large" color={cores.azul}/></View>;
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"} translucent backgroundColor="transparent" />
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={22} color={cores.texto}/>
<TouchableOpacity style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={cores.texto}/>
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Estágios</Text>
<TouchableOpacity
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={fetchDados}
>
<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}/>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<View style={[styles.searchContainer, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={18} color={cores.secundario} />
<TextInput
placeholder="Procurar estagiário..."
placeholderTextColor={cores.secundario}
style={[styles.searchInput, { color: cores.texto }]}
onChangeText={setSearchMain}
/>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]}>
<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} />
</View>
{estagiosAgrupados.map(grupo => (
<View key={grupo.titulo} style={{ marginBottom: 20 }}>
<View style={[styles.turmaSectionHeader, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.turmaSectionText, { color: cores.azul }]}>
{grupo.titulo}
</Text>
{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>
{grupo.dados.map(e => (
<View key={e.id} style={[styles.card, { backgroundColor: cores.card }]}>
<TouchableOpacity style={{ flex: 1 }} 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 || '');
carregarHorarios(e.id);
setPasso(2);
setModalVisible(true);
}}>
<Text style={[styles.cardTitle, { color: cores.texto }]}>{e.alunos?.nome}</Text>
<View style={styles.row}>
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.badgeText, { color: cores.azul }]}>{e.alunos?.turma_curso}</Text>
</View>
<Text style={[styles.cardSub, { color: cores.secundario }]}> {e.horas_diarias || '0h'}/dia</Text>
</View>
<Text style={[styles.empresaText, { color: cores.secundario }]}>🏢 {e.empresas?.nome}</Text>
</TouchableOpacity>
<View key={e.id} style={[styles.estagioCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<TouchableOpacity
style={[styles.btnDelete, { backgroundColor: cores.vermelhoSuave }]}
onPress={() => eliminarEstagio(e.id)}
style={{ flex: 1 }}
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 || '');
setPasso(2); setModalVisible(true);
}}
onLongPress={() => prepararApagar(e.id, e.alunos?.nome)}
>
<Ionicons name="trash-outline" size={18} color={cores.vermelho} />
<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>
<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>
))}
</View>
))}
<TouchableOpacity
style={[styles.btnPrincipal, { backgroundColor: cores.azul }]}
onPress={() => {
setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true);
}}
>
<Ionicons name="add" size={22} color="#fff" />
<Text style={styles.btnPrincipalText}>NOVO ESTÁGIO</Text>
</TouchableOpacity>
</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>
</TouchableOpacity>
</SafeAreaView>
{/* --- Modal Principal (Cadastro/Edição) --- */}
<Modal visible={modalVisible} animationType="slide" transparent>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<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}>
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>
{passo === 1 ? "Seleção de Aluno/Empresa" : "Configuração de Estágio"}
</Text>
{passo === 2 && <Text style={[styles.miniLabel, {marginTop: 2}]}>{alunoSelecionado?.nome}</Text>}
</View>
<TouchableOpacity onPress={handleFecharModal} style={[styles.closeBtn, {backgroundColor: cores.fundo}]}>
<Ionicons name="close" size={22} color={cores.texto} />
</TouchableOpacity>
<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>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{paddingBottom: 40}}>
<ScrollView showsVerticalScrollIndicator={false} nestedScrollEnabled>
{passo === 1 ? (
<View style={{ gap: 15 }}>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>SELECIONE O ALUNO</Text>
<View style={[styles.selectorContainer, { borderColor: cores.borda }]}>
<ScrollView nestedScrollEnabled style={{maxHeight: 200}}>
{Object.keys(alunosAgrupados).map(t => (
<View key={t}>
<Text style={[styles.groupHead, { color: cores.azul, backgroundColor: cores.azulSuave }]}>{t}</Text>
{alunosAgrupados[t].map(a => (
<TouchableOpacity
key={a.id}
style={[styles.item, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]}
onPress={() => setAlunoSelecionado(a)}
>
<Text style={{color: alunoSelecionado?.id === a.id ? cores.azul : cores.texto, fontWeight: alunoSelecionado?.id === a.id ? 'bold' : '400'}}>
{a.nome}
</Text>
{alunoSelecionado?.id === a.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
<View style={{ gap: 20 }}>
<Text style={styles.inputLabel}>Estagiário</Text>
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 200 }]}>
<ScrollView nestedScrollEnabled>
{Object.entries(alunosAgrupados).map(([turma, lista]) => (
<View key={turma}>
<View style={[styles.groupHeader, { backgroundColor: cores.azulSuave }]}><Text style={[styles.groupHeaderText, { color: cores.azul }]}>{turma}</Text></View>
{lista.map(a => (
<TouchableOpacity key={a.id} style={[styles.pickerItem, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]} onPress={() => setAlunoSelecionado(a)}>
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{a.nome}</Text>
</TouchableOpacity>
))}
</View>
@@ -341,22 +304,15 @@ export default function Estagios() {
</ScrollView>
</View>
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 10 }]}>SELECIONE A EMPRESA</Text>
<View style={[styles.selectorContainer, { borderColor: cores.borda }]}>
<ScrollView nestedScrollEnabled style={{maxHeight: 200}}>
{Object.keys(empresasAgrupadas).sort().map(curso => (
<Text style={styles.inputLabel}>Empresa</Text>
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 200 }]}>
<ScrollView nestedScrollEnabled>
{Object.entries(empresasAgrupadas).map(([curso, lista]) => (
<View key={curso}>
<Text style={[styles.groupHead, { color: cores.azul, backgroundColor: cores.azulSuave }]}>{curso.toUpperCase()}</Text>
{empresasAgrupadas[curso].map(emp => (
<TouchableOpacity
key={emp.id}
style={[styles.item, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]}
onPress={() => setEmpresaSelecionada(emp)}
>
<Text style={{color: empresaSelecionada?.id === emp.id ? cores.azul : cores.texto, fontWeight: empresaSelecionada?.id === emp.id ? 'bold' : '400'}}>
{emp.nome}
</Text>
{empresaSelecionada?.id === emp.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{curso}</Text></View>
{lista.map(emp => (
<TouchableOpacity key={emp.id} style={[styles.pickerItem, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]} onPress={() => setEmpresaSelecionada(emp)}>
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{emp.nome}</Text>
</TouchableOpacity>
))}
</View>
@@ -365,73 +321,74 @@ export default function Estagios() {
</View>
</View>
) : (
<View style={{ gap: 16 }}>
<View style={[styles.confirmBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.sectionLabel, { color: cores.azul }]}>DURAÇÃO</Text>
<View style={{flexDirection: 'row', gap: 10, marginTop: 8}}>
<View style={{flex:1}}>
<Text style={styles.miniLabel}>INÍCIO</Text>
<TextInput style={[styles.editInput, { color: cores.texto }]} value={dataInicio} onChangeText={setDataInicio} placeholder="AAAA-MM-DD"/>
</View>
<View style={{flex:1}}>
<Text style={styles.miniLabel}>FIM</Text>
<TextInput style={[styles.editInput, { color: cores.texto }]} value={dataFim} onChangeText={setDataFim} placeholder="AAAA-MM-DD"/>
</View>
</View>
<View style={[styles.badge, { backgroundColor: cores.azul, marginTop: 12, alignSelf: 'flex-start' }]}>
<Text style={[styles.badgeText, { color: '#fff' }]}>Total: {totalHorasDiarias}</Text>
</View>
</View>
<View style={[styles.confirmBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'}}>
<Text style={[styles.sectionLabel, { color: cores.azul }]}>HORÁRIOS</Text>
<TouchableOpacity onPress={() => setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}>
<Ionicons name="add-circle" size={26} color={cores.azul}/>
</TouchableOpacity>
<View style={{ gap: 18 }}>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={styles.groupTitle}>Duração</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>
{horarios.map((h, i) => (
<View key={i} style={styles.horarioRow}>
<TextInput style={[styles.miniInput, {color: cores.texto, flex: 1.2}]} value={h.periodo} onChangeText={(t) => { const n = [...horarios]; n[i].periodo = t; setHorarios(n); }} />
<TextInput style={[styles.miniInput, {color: cores.texto}]} value={h.hora_inicio} onChangeText={(t) => { const n = [...horarios]; n[i].hora_inicio = t; setHorarios(n); }} />
<Text style={{color: cores.secundario, fontSize: 10}}>ATÉ</Text>
<TextInput style={[styles.miniInput, {color: cores.texto}]} value={h.hora_fim} onChangeText={(t) => { const n = [...horarios]; n[i].hora_fim = t; setHorarios(n); }} />
<TouchableOpacity onPress={() => setHorarios(horarios.filter((_, idx) => idx !== i))}>
<Ionicons name="trash" size={18} color={cores.vermelho} />
</TouchableOpacity>
</View>
))}
</View>
<View style={[styles.confirmBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.sectionLabel, { color: cores.azul }]}>DADOS DO TUTOR</Text>
<TextInput style={[styles.editInput, {color: cores.texto, marginTop: 8}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome do Tutor"/>
<TextInput style={[styles.editInput, {color: cores.texto, marginTop: 12}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={styles.groupTitle}>Horários</Text>
<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" />
</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" />
</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"/>
</View>
</View>
)}
</ScrollView>
<View style={styles.modalFooter}>
{passo === 2 && !editandoEstagio && (
<TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.azulSuave }]}>
<Text style={{color: cores.azul, fontWeight: '700'}}>VOLTAR</Text>
</TouchableOpacity>
)}
{passo === 2 && <TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.fundo }]}><Text style={{color: cores.texto, fontWeight: '700'}}>VOLTAR</Text></TouchableOpacity>}
<TouchableOpacity
onPress={() => {
if(passo === 1) {
if(alunoSelecionado && empresaSelecionada) setPasso(2);
else Alert.alert("Atenção", "Seleciona um aluno e uma empresa!");
} else {
salvarEstagio();
}
else Alert.alert("Vai dar merda", "Seleciona o aluno e a empresa primeiro!");
} else salvarEstagio();
}}
style={[styles.btnModalPri, { backgroundColor: cores.azul }]}
>
<Text style={{color:'#fff', fontWeight: '800'}}>
{passo === 1 ? "PRÓXIMO" : "GRAVAR"}
</Text>
<Text style={{color:'#fff', fontWeight: '900'}}>{passo === 1 ? "PRÓXIMO" : "FINALIZAR"}</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
{/* --- NOVO MODAL DE ELIMINAÇÃO MODERNIZADO --- */}
<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>
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Eliminar 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.
</Text>
<View style={styles.deleteFooter}>
<TouchableOpacity style={[styles.deleteBtnCancel, { backgroundColor: cores.fundo }]} onPress={() => setDeleteModalVisible(false)}>
<Text style={[styles.deleteBtnText, { color: cores.texto }]}>CANCELAR</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.deleteBtnConfirm, { backgroundColor: cores.vermelho }]} onPress={confirmarEliminacao}>
<Text style={[styles.deleteBtnText, { color: '#fff' }]}>ELIMINAR</Text>
</TouchableOpacity>
</View>
</View>
@@ -442,44 +399,59 @@ export default function Estagios() {
}
const styles = StyleSheet.create({
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
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
},
tituloGeral: { fontSize: 20, fontWeight: '800' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40, gap: 15 },
searchContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 15, borderWidth: 1, marginBottom: 5 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '500' },
turmaSectionHeader: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 },
turmaSectionText: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
card: { padding: 18, borderRadius: 24, flexDirection: 'row', alignItems: 'center', marginBottom: 2, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 },
cardTitle: { fontSize: 15, fontWeight: '700', marginBottom: 4 },
row: { flexDirection: 'row', alignItems: 'center', gap: 10 },
cardSub: { fontSize: 11, fontWeight: '600' },
empresaText: { fontSize: 12, marginTop: 8, fontWeight: '500' },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
badgeText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' },
btnDelete: { width: 36, height: 36, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
btnPrincipal: { height: 56, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 10, gap: 10, elevation: 4 },
btnPrincipalText: { color: '#fff', fontSize: 14, fontWeight: '800' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' },
modalContent: { borderTopLeftRadius: 32, borderTopRightRadius: 32, padding: 24, height: '88%' },
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: 18, fontWeight: '800' },
closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
sectionLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2 },
selectorContainer: { borderWidth: 1, borderRadius: 16, overflow: 'hidden', marginTop: 8 },
groupHead: { fontSize: 10, padding: 8, fontWeight: '800', textTransform: 'uppercase' },
item: { padding: 15, borderBottomWidth: 0.5, borderColor: 'rgba(0,0,0,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
confirmBox: { borderWidth: 1, borderRadius: 20, padding: 16 },
miniLabel: { fontSize: 9, fontWeight: '800', color: '#94A3B8', marginBottom: 2 },
editInput: { borderBottomWidth: 1, paddingVertical: 6, fontSize: 14, fontWeight: '600', borderColor: 'rgba(0,0,0,0.1)' },
horarioRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 12 },
miniInput: { borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.1)', flex: 1, padding: 6, fontSize: 13, fontWeight: '700', textAlign: 'center' },
modalFooter: { flexDirection: 'row', gap: 12, marginTop: 30, paddingBottom: 20 },
btnModalPri: { flex: 2, height: 54, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
btnModalSec: { flex: 1, height: 54, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
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 },
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%' },
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' },
});

View File

@@ -1,18 +1,19 @@
// app/Professor/Alunos/ListaAlunosProfessor.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
FlatList,
Platform,
SafeAreaView,
RefreshControl,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
View
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
@@ -24,27 +25,33 @@ export interface Aluno {
turma: string;
}
interface TurmaAgrupada {
nome: string;
alunos: Aluno[];
}
const ListaAlunosProfessor = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const insets = useSafeAreaInsets();
// --- ESTADOS ---
const [search, setSearch] = useState('');
const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]);
const [turmas, setTurmas] = useState<TurmaAgrupada[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const azulPetroleo = '#2390a6';
// --- CORES DINÂMICAS ---
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
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)',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
}), [isDarkMode]);
// --- FUNÇÕES ---
const fetchAlunos = async () => {
try {
setLoading(true);
@@ -55,10 +62,7 @@ const ListaAlunosProfessor = memo(() => {
.order('nome', { ascending: true });
if (error) throw error;
if (!data) {
setTurmas([]);
return;
}
if (!data) return setTurmas([]);
const agrupadas: Record<string, Aluno[]> = {};
data.forEach(item => {
@@ -74,213 +78,156 @@ const ListaAlunosProfessor = memo(() => {
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
} catch (err) {
console.error('Erro ao carregar alunos:', err);
console.error('Erro:', err);
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
useEffect(() => { fetchAlunos(); }, []);
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchAlunos();
}, []);
const filteredTurmas = turmas
.map(turma => ({
...turma,
alunos: turma.alunos.filter(a =>
a.nome.toLowerCase().includes(search.toLowerCase()) ||
a.n_escola.includes(search)
),
}))
.filter(t => t.alunos.length > 0);
const filteredTurmas = useMemo(() => {
return turmas
.map(turma => ({
...turma,
alunos: turma.alunos.filter(a =>
a.nome.toLowerCase().includes(search.toLowerCase()) ||
a.n_escola.includes(search)
),
}))
.filter(t => t.alunos.length > 0);
}, [turmas, search]);
return (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* HEADER FIXO */}
<View style={styles.headerFixed}>
<View style={styles.topBar}>
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER IGUAL ÀS EMPRESAS */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => router.back()}
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={22} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.texto} />
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Alunos</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
Gestão de turmas e estágios
</Text>
</View>
<TouchableOpacity
style={[styles.refreshBtn, { backgroundColor: cores.azul }]}
onPress={fetchAlunos}
>
<Ionicons name="reload" size={22} color="#fff" />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Alunos</Text>
<View style={{ width: 42 }} />
</View>
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.secundario} />
<TextInput
placeholder="Procurar aluno ou Nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
style={[styles.searchInput, { color: cores.texto }]}
/>
{/* SEARCH BAR IGUAL ÀS EMPRESAS */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar por nome ou nº..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
</View>
{loading ? (
<View style={styles.centered}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.section}>
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.turmaLabel, { color: cores.azul }]}>
{item.nome} {item.alunos.length} Alunos
</Text>
{loading && !refreshing ? (
<View style={styles.loadingCenter}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<FlatList
data={filteredTurmas}
keyExtractor={item => item.nome}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderItem={({ item }) => (
<View style={{ marginBottom: 25 }}>
{/* Cabeçalho da Secção (Turma) */}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{item.nome}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
{item.alunos.map((aluno) => (
<TouchableOpacity
key={aluno.id}
activeOpacity={0.8}
style={[styles.alunoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.push({
pathname: '/Professor/Alunos/DetalhesAluno',
params: { alunoId: aluno.id }
})}
>
<View style={[styles.alunoIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="person" size={22} color={cores.azul} />
</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>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.4} />
</TouchableOpacity>
))}
</View>
{item.alunos.map(aluno => (
<TouchableOpacity
key={aluno.id}
style={[styles.card, { backgroundColor: cores.card }]}
onPress={() =>
router.push({
pathname: '/Professor/Alunos/DetalhesAluno',
params: { alunoId: aluno.id },
})
}
>
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.avatarText, { 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 }]}> Escola: {aluno.n_escola}</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</TouchableOpacity>
))}
</View>
)}
/>
)}
</SafeAreaView>
)}
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>
</View>
)}
/>
)}
</SafeAreaView>
</View>
);
});
// --- ESTILOS ---
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'
},
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' },
alunoInfo: { flex: 1, marginLeft: 15 },
alunoNome: { fontSize: 16, fontWeight: '800' },
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 },
idText: { fontSize: 13, fontWeight: '600' },
});
export default ListaAlunosProfessor;

View File

@@ -1,19 +1,21 @@
// app/Professor/Empresas/DetalhesEmpresa.tsx
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { memo, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TextInputProps,
TouchableOpacity,
View
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
@@ -26,24 +28,13 @@ export interface Empresa {
curso: string;
}
interface AlunoVinculado {
id: string;
nome: string;
}
interface InfoItemProps extends TextInputProps {
label: string;
value: string;
icon: keyof typeof Ionicons.glyphMap;
editable: boolean;
onChangeText?: (v: string) => void;
cores: any;
}
const DetalhesEmpresa = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const params = useLocalSearchParams();
const insets = useSafeAreaInsets();
const azulPetroleo = '#2390a6';
const empresaOriginal: Empresa = useMemo(() => {
if (!params.empresa) return {} as Empresa;
@@ -56,57 +47,40 @@ const DetalhesEmpresa = memo(() => {
}, [params.empresa]);
const [empresaLocal, setEmpresaLocal] = useState<Empresa>({ ...empresaOriginal });
const [alunos, setAlunos] = useState<AlunoVinculado[]>([]);
const [alunos, setAlunos] = useState<{ id: string; nome: string }[]>([]);
const [loadingAlunos, setLoadingAlunos] = useState(true);
const [editando, setEditando] = useState(false);
const [loading, setLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
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)',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
vermelho: '#EF4444',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.4)',
}), [isDarkMode]);
useEffect(() => {
if (empresaLocal.id) {
carregarAlunos();
}
if (empresaLocal.id) carregarAlunos();
}, [empresaLocal.id]);
async function carregarAlunos() {
try {
setLoadingAlunos(true);
const { data: estagios } = await supabase.from('estagios').select('aluno_id').eq('empresa_id', empresaLocal.id);
if (!estagios || estagios.length === 0) { setAlunos([]); return; }
const { data: estagios, error: errEstagios } = await supabase
.from('estagios')
.select('aluno_id')
.eq('empresa_id', empresaLocal.id);
if (errEstagios) throw errEstagios;
if (!estagios || estagios.length === 0) {
setAlunos([]);
return;
}
const ids = estagios.map(e => e.aluno_id).filter(id => id !== null);
const { data: listaAlunos, error: errAlunos } = await supabase
.from('alunos')
.select('id, nome')
.in('id', ids);
if (errAlunos) throw errAlunos;
const { data: listaAlunos } = await supabase.from('alunos').select('id, nome').in('id', ids);
setAlunos(listaAlunos || []);
} catch (error: any) {
console.error("Erro ao carregar lista:", error.message);
} catch (error) {
setAlunos([]);
} finally {
setLoadingAlunos(false);
@@ -116,177 +90,218 @@ const DetalhesEmpresa = memo(() => {
const handleSave = async () => {
try {
setLoading(true);
const { error } = await supabase
.from('empresas')
.update({
nome: empresaLocal.nome,
morada: empresaLocal.morada,
tutor_nome: empresaLocal.tutor_nome,
tutor_telefone: empresaLocal.tutor_telefone,
curso: empresaLocal.curso,
})
.eq('id', empresaLocal.id);
const { error } = await supabase.from('empresas').update({
nome: empresaLocal.nome,
morada: empresaLocal.morada,
tutor_nome: empresaLocal.tutor_nome,
tutor_telefone: empresaLocal.tutor_telefone,
curso: empresaLocal.curso,
}).eq('id', empresaLocal.id);
if (error) throw error;
setEditando(false);
Alert.alert('Sucesso', 'Dados atualizados!');
} catch (error: any) {
Alert.alert('Erro', 'Falha ao guardar.');
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
const handleDelete = () => {
Alert.alert('Apagar Entidade', `Confirmas a remoção da ${empresaLocal.nome}?`, [
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Apagar',
style: 'destructive',
onPress: async () => {
await supabase.from('empresas').delete().eq('id', empresaLocal.id);
router.back();
}
}
]);
const confirmDelete = async () => {
try {
setLoading(true);
const { error } = await supabase.from('empresas').delete().eq('id', empresaLocal.id);
if (error) throw error;
setShowDeleteModal(false);
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);
}
};
return (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<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>
</View>
)}
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* 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} />
</View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Tens a certeza?</Text>
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>
Esta ação irá remover permanentemente a entidade <Text style={{fontWeight: '800'}}>{empresaLocal.nome}</Text>.
</Text>
<View style={styles.modalButtons}>
<TouchableOpacity
style={[styles.modalBtn, { backgroundColor: cores.fundo }]}
onPress={() => setShowDeleteModal(false)}
>
<Text style={[styles.modalBtnTxt, { color: cores.texto }]}>Cancelar</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalBtn, { backgroundColor: cores.vermelho }]}
onPress={confirmDelete}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.modalBtnTxt, { color: '#fff' }]}>Apagar</Text>}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<SafeAreaView style={styles.safe} edges={['top']}>
<View style={styles.header}>
<TouchableOpacity style={[styles.btnCircle, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
<TouchableOpacity
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]} numberOfLines={1}>
Detalhes da Entidade
</Text>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Detalhes da Entidade</Text>
<TouchableOpacity
style={[styles.btnCircle, { backgroundColor: editando ? cores.vermelho : cores.card }]}
onPress={() => setEditando(!editando)}
style={[styles.btnAction, { backgroundColor: editando ? cores.vermelho : cores.card, borderColor: editando ? cores.vermelho : cores.borda }]}
onPress={() => {
if(editando) setEmpresaLocal({...empresaOriginal});
setEditando(!editando);
}}
>
<Ionicons name={editando ? "close" : "pencil"} size={20} color={editando ? "#fff" : cores.azul} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 20 }]} showsVerticalScrollIndicator={false}>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Informação da Empresa</Text>
<InfoItem label="Nome" value={empresaLocal.nome} icon="business" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} />
<InfoItem label="Curso" value={empresaLocal.curso} icon="book" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} />
<InfoItem label="Morada" value={empresaLocal.morada} icon="location" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} />
<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>
<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 />
</View>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Contacto do Tutor</Text>
<InfoItem label="Nome" value={empresaLocal.tutor_nome} icon="person" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} />
<InfoItem label="Telefone" value={empresaLocal.tutor_telefone} icon="call" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" />
<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>
<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" />
</View>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<View style={styles.alunosHeader}>
<Text style={[styles.sectionLabel, { color: cores.secundario, marginBottom: 0 }]}>Alunos em Estágio</Text>
<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.badgeText, { color: cores.azul }]}>
{loadingAlunos ? '...' : alunos.length}
</Text>
<Text style={[styles.badgeTxt, { color: cores.azul }]}>{loadingAlunos ? '...' : alunos.length}</Text>
</View>
</View>
{loadingAlunos ? (
<ActivityIndicator size="small" color={cores.azul} style={{ marginVertical: 10 }} />
) : alunos.length > 0 ? (
alunos.map((aluno, index) => (
<View
key={index}
style={[styles.alunoItem, { borderBottomColor: cores.borda, borderBottomWidth: index === alunos.length - 1 ? 0 : 1 }]}
>
<View style={[styles.alunoAvatar, { backgroundColor: cores.azulSuave }]}>
<Text style={{ color: cores.azul, fontWeight: 'bold', fontSize: 12 }}>{aluno.nome.charAt(0).toUpperCase()}</Text>
{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>
</View>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
<Text style={[styles.alunoName, { color: cores.texto }]}>{aluno.nome}</Text>
</View>
))
) : (
<View style={styles.emptyAlunos}>
<Ionicons name="people-outline" size={30} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 5, fontSize: 13 }}>Nenhum aluno associado</Text>
</View>
)}
) : <Text style={[styles.empty, { color: cores.secundario }]}>Sem alunos vinculados.</Text>}
</View>
{editando ? (
<TouchableOpacity style={[styles.btnPrincipal, { backgroundColor: cores.azul }]} onPress={handleSave} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.btnPrincipalText}>Guardar Alterações</Text>}
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.btnDelete, { backgroundColor: cores.vermelhoSuave }]} onPress={handleDelete}>
<Ionicons name="trash-outline" size={20} color={cores.vermelho} />
<Text style={[styles.btnDeleteText, { color: cores.vermelho }]}>Remover Entidade</Text>
</TouchableOpacity>
)}
<View style={{ marginTop: 5 }}>
{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>
)}
</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>
)}
</View>
</ScrollView>
</SafeAreaView>
</View>
</KeyboardAvoidingView>
);
});
const InfoItem = ({ label, value, icon, editable, onChangeText, cores, ...props }: InfoItemProps) => (
<View style={styles.infoWrapper}>
<View style={[styles.infoIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={18} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.infoLabel, { color: cores.secundario }]}>{label}</Text>
{editable ? (
<TextInput
style={[styles.infoInput, { color: cores.texto, borderBottomColor: cores.azul }]}
value={value}
onChangeText={onChangeText}
{...props}
/>
) : (
<Text style={[styles.infoValue, { color: cores.texto }]}>{value || '---'}</Text>
)}
</View>
const ModernField = ({ label, value, editable, cores, ...props }: any) => (
<View style={styles.fieldContainer}>
<Text style={[styles.fieldLabel, { color: cores.secundario }]}>{label}</Text>
{editable ? (
<TextInput style={[styles.input, { color: cores.texto, backgroundColor: cores.fundo, borderColor: cores.azul }]} value={value} selectionColor={cores.azul} {...props} />
) : (
<View style={[styles.readOnly, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.readOnlyTxt, { color: cores.texto }]}>{value || '---'}</Text>
</View>
)}
</View>
);
const styles = StyleSheet.create({
safe: { flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
btnCircle: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 },
tituloGeral: { fontSize: 18, fontWeight: '800', flex: 1, textAlign: 'center', marginHorizontal: 15 },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40, gap: 15 },
card: { padding: 20, borderRadius: 24, elevation: 2 },
sectionLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 1 },
infoWrapper: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 },
infoIcon: { width: 36, height: 36, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
infoLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase', marginBottom: 2 },
infoValue: { fontSize: 15, fontWeight: '600' },
infoInput: { fontSize: 15, fontWeight: '600', borderBottomWidth: 1, paddingVertical: 2 },
alunosHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
badgeText: { fontSize: 12, fontWeight: '800' },
alunoItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 },
alunoAvatar: { width: 28, height: 28, borderRadius: 8, justifyContent: 'center', alignItems: 'center', marginRight: 10 },
alunoNome: { flex: 1, fontSize: 14, fontWeight: '600' },
emptyAlunos: { alignItems: 'center', paddingVertical: 10 },
btnPrincipal: { height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center', marginTop: 10 },
btnPrincipalText: { color: '#fff', fontSize: 16, fontWeight: '800' },
btnDelete: { height: 56, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 10, gap: 8 },
btnDeleteText: { fontSize: 16, fontWeight: '700' }
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' }
});
export default DetalhesEmpresa;

View File

@@ -1,3 +1,4 @@
// app/Professor/Empresas/ListaEmpresasProfessor.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
@@ -21,7 +22,6 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
// --- INTERFACES ---
export interface Empresa {
id: number;
nome: string;
@@ -36,32 +36,28 @@ const ListaEmpresasProfessor = memo(() => {
const router = useRouter();
const insets = useSafeAreaInsets();
// --- ESTADOS ---
const [search, setSearch] = useState('');
const [empresas, setEmpresas] = useState<Empresa[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// Estados do Formulário (Modal)
const [modalVisible, setModalVisible] = useState(false);
const [nome, setNome] = useState('');
const [morada, setMorada] = useState('');
const [tutorNome, setTutorNome] = useState('');
const [tutorTelefone, setTutorTelefone] = useState('');
const [curso, setCurso] = useState('');
// --- CORES DINÂMICAS ---
// Estados do Formulário
const [form, setForm] = useState({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
const azulPetroleo = '#2390a6';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
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)',
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
branco: '#FFFFFF'
}), [isDarkMode]);
// --- FUNÇÕES ---
const fetchEmpresas = async () => {
try {
setLoading(true);
@@ -70,27 +66,23 @@ const ListaEmpresasProfessor = memo(() => {
.select('*')
.order('curso', { ascending: true })
.order('nome', { ascending: true });
if (error) throw error;
setEmpresas(data || []);
} catch (error: any) {
Alert.alert('Erro ao carregar', error.message);
Alert.alert('Erro', 'Não foi possível carregar as empresas.');
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchEmpresas();
}, []);
useEffect(() => { fetchEmpresas(); }, []);
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchEmpresas();
}, []);
// --- CORREÇÃO DO AGRUPAMENTO ---
const secoesAgrupadas = useMemo(() => {
const filtradas = empresas.filter(e =>
e.nome?.toLowerCase().includes(search.toLowerCase()) ||
@@ -98,9 +90,7 @@ const ListaEmpresasProfessor = memo(() => {
);
const grupos = filtradas.reduce((acc: { [key: string]: Empresa[] }, empresa) => {
// Normalizamos o nome do curso para evitar duplicados (Tudo maiúsculas e sem espaços extras)
const cursoKey = (empresa.curso || 'Sem Curso').trim().toUpperCase();
const cursoKey = (empresa.curso || 'Outros').trim().toUpperCase();
if (!acc[cursoKey]) acc[cursoKey] = [];
acc[cursoKey].push(empresa);
return acc;
@@ -113,8 +103,8 @@ const ListaEmpresasProfessor = memo(() => {
}, [search, empresas]);
const criarEmpresa = async () => {
if (!nome || !morada || !tutorNome || !tutorTelefone || !curso) {
Alert.alert('Atenção', 'Preenche todos os campos para não dar merda.');
if (!form.nome || !form.morada || !form.tutorNome || !form.tutorTelefone || !form.curso) {
Alert.alert('Atenção', 'Preenche todos os campos obrigatórios.');
return;
}
@@ -123,23 +113,21 @@ const ListaEmpresasProfessor = memo(() => {
const { data, error } = await supabase
.from('empresas')
.insert([{
nome: nome.trim(),
morada: morada.trim(),
tutor_nome: tutorNome.trim(),
tutor_telefone: tutorTelefone.trim(),
curso: curso.trim(), // O trim aqui já ajuda na base de dados
nome: form.nome.trim(),
morada: form.morada.trim(),
tutor_nome: form.tutorNome.trim(),
tutor_telefone: form.tutorTelefone.trim(),
curso: form.curso.trim().toUpperCase(),
}])
.select();
if (error) throw error;
setEmpresas(prev => [...prev, data![0]]);
setModalVisible(false);
setNome(''); setMorada(''); setTutorNome(''); setTutorTelefone(''); setCurso('');
Alert.alert('Sucesso', 'Nova empresa registada!');
setForm({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
Alert.alert('Sucesso', 'Entidade registada!');
} catch (error: any) {
Alert.alert('Erro ao criar', error.message);
Alert.alert('Erro', error.message);
} finally {
setLoading(false);
}
@@ -147,27 +135,23 @@ const ListaEmpresasProfessor = memo(() => {
return (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
translucent
backgroundColor="transparent"
/>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
<SafeAreaView style={styles.safe} edges={['top']}>
{/* HEADER */}
{/* HEADER MODERNIZADO */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: cores.card }]}
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.texto} />
</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 no total
{empresas.length} entidades ativas
</Text>
</View>
@@ -175,17 +159,17 @@ const ListaEmpresasProfessor = memo(() => {
style={[styles.addBtn, { backgroundColor: cores.azul }]}
onPress={() => setModalVisible(true)}
>
<Ionicons name="add" size={26} color="#fff" />
<Ionicons name="add" size={28} color="#fff" />
</TouchableOpacity>
</View>
{/* SEARCH BAR */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.secundario} />
<Ionicons name="search" size={20} color={cores.azul} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar por nome ou curso..."
placeholder="Pesquisar entidade ou curso..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
@@ -193,7 +177,6 @@ const ListaEmpresasProfessor = memo(() => {
</View>
</View>
{/* CONTEÚDO / LISTA */}
{loading && !refreshing ? (
<View style={styles.loadingCenter}>
<ActivityIndicator size="large" color={cores.azul} />
@@ -203,29 +186,19 @@ const ListaEmpresasProfessor = memo(() => {
sections={secoesAgrupadas}
keyExtractor={item => item.id.toString()}
stickySectionHeadersEnabled={false}
contentContainerStyle={[
styles.listPadding,
{ paddingBottom: insets.bottom + 100 }
]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />
}
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
renderSectionHeader={({ section: { title } }) => (
<View style={styles.sectionHeader}>
<View style={[styles.sectionLine, { backgroundColor: cores.azul }]} />
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{title}</Text>
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
</View>
)}
renderItem={({ item }) => (
<TouchableOpacity
activeOpacity={0.7}
style={[styles.empresaCard, { backgroundColor: cores.card }]}
onPress={() =>
router.push({
pathname: '/Professor/Empresas/DetalhesEmpresa',
params: { empresa: JSON.stringify(item) }
})
}
activeOpacity={0.8}
style={[styles.empresaCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', params: { empresa: JSON.stringify(item) } })}
>
<View style={[styles.empresaIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="business" size={22} color={cores.azul} />
@@ -234,60 +207,50 @@ const ListaEmpresasProfessor = memo(() => {
<View style={styles.empresaInfo}>
<Text style={[styles.empresaNome, { color: cores.texto }]}>{item.nome}</Text>
<View style={styles.tutorRow}>
<Ionicons name="person-outline" size={14} color={cores.secundario} />
<Ionicons name="person-circle-outline" size={14} color={cores.secundario} />
<Text style={[styles.tutorText, { color: cores.secundario }]}>{item.tutor_nome}</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.borda} />
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.4} />
</TouchableOpacity>
)}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="search-circle-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10 }}>Nenhuma empresa encontrada.</Text>
<Ionicons name="business-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '600' }}>Sem resultados.</Text>
</View>
)}
/>
)}
</SafeAreaView>
{/* MODAL DE CRIAÇÃO */}
{/* MODAL DE CRIAÇÃO PREMIUM */}
<Modal visible={modalVisible} animationType="slide" transparent>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={[
styles.modalContent,
{ backgroundColor: cores.card, paddingBottom: Platform.OS === 'ios' ? insets.bottom + 20 : 30 }
]}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Registar Empresa</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Ionicons name="close-circle" size={28} color={cores.secundario} />
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Nova Empresa</Text>
<Text style={[styles.modalSub, { color: cores.secundario }]}>Preenche os detalhes da entidade</Text>
</View>
<TouchableOpacity onPress={() => setModalVisible(false)} style={styles.closeBtn}>
<Ionicons name="close" size={24} color={cores.texto} />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
<ModernInput label="Nome da Entidade" icon="business-outline" value={nome} onChangeText={setNome} cores={cores} />
<ModernInput label="Morada" icon="location-outline" value={morada} onChangeText={setMorada} cores={cores} />
<ModernInput label="Curso (ex: GPSI, Multimédia)" icon="book-outline" value={curso} onChangeText={setCurso} cores={cores} />
<ModernInput label="Tutor de Empresa" icon="person-outline" value={tutorNome} onChangeText={setTutorNome} cores={cores} />
<ModernInput
label="Telefone do Tutor"
icon="call-outline"
value={tutorTelefone}
onChangeText={setTutorTelefone}
keyboardType="phone-pad"
cores={cores}
/>
<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} />
<TouchableOpacity
style={[styles.saveBtn, { backgroundColor: cores.azul }]}
onPress={criarEmpresa}
>
<Text style={styles.saveBtnText}>Guardar Empresa</Text>
<View style={styles.divider} />
<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} />
<TouchableOpacity style={[styles.saveBtn, { backgroundColor: cores.azul }]} onPress={criarEmpresa}>
<Text style={styles.saveBtnText}>Registar Entidade</Text>
</TouchableOpacity>
</ScrollView>
</View>
@@ -301,48 +264,47 @@ const ModernInput = ({ label, icon, cores, ...props }: any) => (
<View style={styles.inputWrapper}>
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
<View style={[styles.inputContainer, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 10 }} />
<TextInput
{...props}
style={[styles.textInput, { color: cores.texto }]}
placeholderTextColor={cores.secundario}
/>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 12 }} />
<TextInput {...props} style={[styles.textInput, { color: cores.texto }]} placeholderTextColor={cores.secundario} />
</View>
</View>
);
const styles = StyleSheet.create({
safe: { flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 15, paddingTop: Platform.OS === 'android' ? 10 : 0 },
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
headerTitle: { fontSize: 22, fontWeight: '800' },
headerSubtitle: { fontSize: 13, fontWeight: '500' },
addBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 4 },
searchSection: { paddingHorizontal: 20, marginBottom: 10 },
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 15, borderWidth: 1 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
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 },
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' },
loadingCenter: { marginTop: 50, alignItems: 'center' },
emptyContainer: { marginTop: 80, alignItems: 'center', justifyContent: 'center' },
emptyContainer: { marginTop: 80, alignItems: 'center' },
listPadding: { paddingHorizontal: 20 },
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 12, marginLeft: 5 },
sectionLine: { width: 4, height: 16, borderRadius: 2, marginRight: 8 },
sectionTitle: { fontSize: 14, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 18, marginBottom: 10, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } },
empresaIcon: { width: 46, height: 46, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
empresaInfo: { flex: 1, marginLeft: 12 },
empresaNome: { fontSize: 15, fontWeight: '700' },
tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 },
tutorText: { fontSize: 12, fontWeight: '500' },
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 25, maxHeight: '90%' },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
modalTitle: { fontSize: 20, fontWeight: '800' },
inputWrapper: { marginBottom: 15 },
inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 },
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, height: 48, borderRadius: 12, borderWidth: 1 },
textInput: { flex: 1, fontSize: 15, fontWeight: '600' },
saveBtn: { height: 52, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginTop: 15 },
saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }
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' },
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 },
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 }
});
export default ListaEmpresasProfessor;

View File

@@ -1,9 +1,10 @@
// app/Professor/PerfilProfessor.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Animated,
ScrollView,
StatusBar,
StyleSheet,
@@ -25,7 +26,6 @@ interface PerfilData {
residencia: string;
tipo: string;
curso: string;
idade?: number;
}
export default function PerfilProfessor() {
@@ -37,16 +37,32 @@ 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), []);
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: '#3B82F6',
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
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',
}), [isDarkMode]);
useEffect(() => {
@@ -67,7 +83,7 @@ export default function PerfilProfessor() {
setPerfil(data);
}
} catch (error: any) {
Alert.alert('Erro', 'Não foi possível carregar os dados.');
showAlert('Não foi possível carregar os dados.', 'error');
} finally {
setLoading(false);
}
@@ -89,9 +105,9 @@ export default function PerfilProfessor() {
if (error) throw error;
setEditando(false);
Alert.alert('Sucesso', 'Perfil atualizado!');
showAlert('Perfil atualizado com sucesso!', 'success');
} catch (error: any) {
Alert.alert('Erro ao gravar', 'Verifica a coluna "curso" no Supabase para não dar merda.');
showAlert('Erro ao gravar dados no servidor.', 'error');
}
};
@@ -112,6 +128,21 @@ 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,
{
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>
)}
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* HEADER */}
@@ -139,7 +170,7 @@ export default function PerfilProfessor() {
</View>
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.userRole, { color: cores.secundario }]}>
{perfil?.curso || 'Professor'}
{perfil?.curso || 'Professor / Tutor'}
</Text>
</View>
@@ -206,7 +237,7 @@ export default function PerfilProfessor() {
<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.borda} />
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
</TouchableOpacity>
<TouchableOpacity
@@ -243,7 +274,7 @@ const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
{
backgroundColor: cores.fundo,
borderColor: editable ? cores.azul : cores.borda,
opacity: editable ? 1 : 0.7
opacity: editable ? 1 : 0.8
}
]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 10 }} />
@@ -260,6 +291,21 @@ 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',
@@ -280,19 +326,19 @@ const styles = StyleSheet.create({
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: 48, borderRadius: 14, borderWidth: 1 },
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, height: 52, borderRadius: 16, borderWidth: 1.5 },
textInput: { flex: 1, fontSize: 15, fontWeight: '600' },
row: { flexDirection: 'row' },
actionsContainer: { gap: 10 },
actionsContainer: { gap: 12 },
menuItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderRadius: 18,
padding: 14,
borderRadius: 20,
elevation: 1
},
menuIcon: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
menuIcon: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
menuText: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '700' },
cancelBtn: { marginTop: 20, alignItems: 'center' },
cancelBtn: { marginTop: 25, alignItems: 'center' },
cancelText: { fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' }
});

View File

@@ -1,11 +1,10 @@
// app/Professor/ProfessorMenu.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Dimensions,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
@@ -13,28 +12,31 @@ 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';
const { width } = Dimensions.get('window');
// Tipagem para os ícones do Ionicons
type IonIconName = keyof typeof Ionicons.glyphMap;
export default function ProfessorMenu() {
const router = useRouter();
const { isDarkMode } = useTheme();
const insets = useSafeAreaInsets(); // Para ajustes finos se necessário
const [nome, setNome] = useState<string>('');
const [loading, setLoading] = useState(true);
const cores = {
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
const azulPetroleo = '#2390a6';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
azul: '#3B82F6',
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
};
azul: azulPetroleo,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
}), [isDarkMode]);
useEffect(() => {
async function obterNome() {
@@ -46,13 +48,11 @@ export default function ProfessorMenu() {
.select('nome')
.eq('id', user.id)
.single();
if (error) throw error;
// Alterado para carregar o nome completo sem divisões
if (data?.nome) setNome(data.nome);
}
} catch (err: any) {
console.error("Erro ao carregar nome:", err.message);
console.error(err.message);
} finally {
setLoading(false);
}
@@ -61,199 +61,111 @@ export default function ProfessorMenu() {
}, []);
return (
<SafeAreaView style={[styles.container, { backgroundColor: cores.fundo }]}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor="transparent"
translucent
/>
// 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}
contentContainerStyle={[
styles.content,
{ paddingBottom: insets.bottom + 20 } // Garante que o último card não cole no fundo
]}
showsVerticalScrollIndicator={false}
>
{/* HEADER COM NOME COMPLETO */}
{/* HEADER */}
<View style={styles.header}>
<Text style={[styles.welcome, { color: cores.textoSecundario }]}>Bem-vindo,</Text>
{loading ? (
<ActivityIndicator size="small" color={cores.azul} style={{ alignSelf: 'flex-start', marginTop: 8 }} />
) : (
<Text style={[styles.name, { color: cores.texto }]}>
{nome || 'Professor'}
</Text>
)}
<View style={styles.headerRow}>
<View>
<Text style={[styles.welcome, { color: cores.textoSecundario }]}>Bem-vindo,</Text>
{loading ? (
<ActivityIndicator size="small" color={cores.azul} style={{ marginTop: 8, alignSelf: 'flex-start' }} />
) : (
<Text style={[styles.name, { color: cores.texto }]}>{nome || 'Professor'}</Text>
)}
</View>
<TouchableOpacity
onPress={() => router.push('/Professor/PerfilProf')}
style={[styles.avatarMini, { backgroundColor: cores.azul }]}
>
<Text style={styles.avatarTxt}>{nome?.charAt(0).toUpperCase()}</Text>
</TouchableOpacity>
</View>
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
<View style={[styles.dot, { backgroundColor: cores.azul }]} />
<Text style={[styles.subtitle, { color: cores.azul }]}>Painel de Gestão de Estágios</Text>
<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>
</View>
{/* GRID DE OPÇÕES */}
{/* GRID */}
<View style={styles.grid}>
<MenuCard
icon="person-outline"
title="Perfil"
subtitle="Dados pessoais"
onPress={() => router.push('/Professor/PerfilProf')}
cores={cores}
/>
<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} />
</View>
<MenuCard
icon="settings-outline"
title="Definições"
subtitle="Configurações"
onPress={() => router.push('/Professor/defenicoes2')}
cores={cores}
/>
<MenuCard
icon="document-text-outline"
title="Sumários"
subtitle="Verificar registos"
onPress={() => router.push('/Professor/Alunos/Sumarios')}
cores={cores}
/>
<MenuCard
icon="close-circle-outline"
title="Faltas"
subtitle="Verificar faltas"
onPress={() => router.push('/Professor/Alunos/Faltas')}
cores={cores}
/>
<MenuCard
icon="checkmark-circle-outline"
title="Presenças"
subtitle="Verificar presenças"
onPress={() => router.push('/Professor/Alunos/Presencas')}
cores={cores}
/>
<MenuCard
icon="briefcase-outline"
title="Estágios"
subtitle="Criar / Editar"
onPress={() => router.push('/Professor/Alunos/Estagios')}
cores={cores}
/>
<MenuCard
icon="people-outline"
title="Alunos"
subtitle="Lista de alunos"
onPress={() => router.push('/Professor/Alunos/ListaAlunos')}
cores={cores}
/>
<MenuCard
icon="business-outline"
title="Empresas"
subtitle="Lista de empresas"
onPress={() => router.push('/Professor/Empresas/ListaEmpresas')}
cores={cores}
/>
<View style={styles.footer}>
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>EPVC Gestão de Estágios</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
function MenuCard({ icon, title, subtitle, onPress, cores }: {
icon: IonIconName;
title: string;
subtitle: string;
onPress: () => void;
cores: any
}) {
// O MenuCard permanece igual, apenas garantindo consistência
function MenuCard({ icon, title, subtitle, onPress, cores }: any) {
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: cores.card }]}
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={onPress}
activeOpacity={0.7}
activeOpacity={0.8}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={24} color={cores.azul} />
<Ionicons name={icon} size={22} color={cores.azul} />
</View>
<Text style={[styles.cardTitle, { color: cores.texto }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]} numberOfLines={1}>{subtitle}</Text>
<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} />
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 10 : 10,
},
content: {
padding: 20,
},
header: {
marginBottom: 32,
},
welcome: {
fontSize: 14,
fontWeight: '600',
letterSpacing: 0.5,
},
name: {
fontSize: 26, // Reduzi ligeiramente o tamanho para nomes completos não quebrarem
fontWeight: '800',
letterSpacing: -0.5,
marginBottom: 12,
},
badge: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 8,
},
subtitle: {
fontSize: 11,
fontWeight: '800',
textTransform: 'uppercase',
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
content: { padding: 20 },
header: { marginBottom: 30 },
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 },
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
card: {
width: (width - 55) / 2,
borderRadius: 24,
width: (width - 55) / 2, // Ajuste para melhor margem entre cards
borderRadius: 28,
padding: 18,
marginBottom: 15,
elevation: 4,
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowOpacity: 0.04,
shadowRadius: 10,
elevation: 2,
position: 'relative',
overflow: 'hidden'
},
iconBox: {
width: 44,
height: 44,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 14,
},
cardTitle: {
fontSize: 15,
fontWeight: '700',
},
cardSubtitle: {
fontSize: 12,
marginTop: 3,
fontWeight: '500',
},
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 }
});

View File

@@ -1,10 +1,10 @@
// app/Definicoes.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useMemo, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import {
Alert,
Animated,
Linking,
Platform,
ScrollView,
StatusBar,
StyleSheet,
@@ -15,161 +15,177 @@ import {
} 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 insets = useSafeAreaInsets();
const [notificacoes, setNotificacoes] = useState(true);
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';
const cores = useMemo(() => ({
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
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)',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
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 abrirURL = (url: string) => Linking.openURL(url);
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 (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* 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 */}
{/* HEADER ESTILIZADO */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
style={[styles.btnVoltar, { backgroundColor: cores.card, borderColor: cores.borda }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
<Ionicons name="chevron-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{ width: 42 }} />
<View style={{ width: 45 }} />
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* SECÇÃO PREFERÊNCIAS */}
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Preferências</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
{/* 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.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="notifications-outline" size={20} color={cores.azul} />
<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</Text>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações Push</Text>
<Switch
value={notificacoes}
onValueChange={setNotificacoes}
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={Platform.OS === 'ios' ? undefined : '#f4f3f4'}
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.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={isDarkMode ? "moon-outline" : "sunny-outline"} size={20} color={cores.azul} />
<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 }]}>Modo Escuro</Text>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Interface Escura</Text>
<Switch
value={isDarkMode}
onValueChange={toggleTheme}
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={Platform.OS === 'ios' ? undefined : '#f4f3f4'}
trackColor={{ false: '#CBD5E1', true: cores.azul }}
thumbColor="#FFFFFF"
/>
</View>
</View>
{/* SECÇÃO SUPORTE */}
<Text style={[styles.sectionTitle, { color: cores.secundario, marginTop: 25 }]}>Suporte e Contactos</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<TouchableOpacity
style={[styles.item, { borderBottomColor: cores.borda }]}
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="business-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>Direção</Text>
<Text style={{ color: cores.secundario, fontSize: 12 }}>epvc@epvc.pt</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.item, { borderBottomColor: cores.borda }]}
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="mail-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>Secretaria</Text>
<Text style={{ color: cores.secundario, fontSize: 12 }}>secretaria@epvc.pt</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</TouchableOpacity>
<TouchableOpacity
style={styles.item}
onPress={() => abrirURL('tel:252641805')}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="call-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>Telefone</Text>
<Text style={{ color: cores.secundario, fontSize: 12 }}>252 641 805</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</TouchableOpacity>
{/* 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>
{/* SECÇÃO INFO & SAIR */}
<View style={[styles.card, { backgroundColor: cores.card, marginTop: 25 }]}>
{/* 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.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="information-circle-outline" size={20} color={cores.azul} />
<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 da App</Text>
<Text style={{ color: cores.secundario, fontWeight: '600' }}>26.3.11</Text>
<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.iconBox, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out-outline" size={22} color={cores.vermelho} />
<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: '700' }]}>Terminar Sessão</Text>
<Ionicons name="chevron-forward" size={16} color={cores.vermelho} />
<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>
<Text style={[styles.footerText, { color: cores.secundario }]}>Escola Profissional de Vila do Conde © 2026</Text>
<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>
@@ -177,24 +193,39 @@ 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 }]}
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 },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 15
},
btnVoltar: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4 },
tituloGeral: { fontSize: 20, fontWeight: '800' },
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
sectionTitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 10, marginLeft: 5, letterSpacing: 1 },
card: { borderRadius: 24, paddingHorizontal: 16, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, borderBottomWidth: 1 },
iconBox: { width: 38, height: 38, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
itemTexto: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' },
footerText: { textAlign: 'center', marginTop: 30, fontSize: 12, fontWeight: '600', opacity: 0.5 }
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 },
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,41 +1,36 @@
// app/forgot-password.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
View,
} from 'react-native';
import { useTheme } from '../../themecontext'; // Tema global
import { supabase } from '../lib/supabase';
import { supabase } from '../../app/lib/supabase';
export default function ForgotPassword() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const { isDarkMode } = useTheme(); // pega o tema global
const [isFocused, setIsFocused] = useState(false);
// ESTADOS PARA OS AVISOS MODERNOS
const [status, setStatus] = useState<{ type: 'error' | 'success'; msg: string } | null>(null);
const cores = useMemo(() => ({
fundo: isDarkMode ? '#121212' : '#f8f9fa',
container: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#2d3436',
textoSecundario: isDarkMode ? '#adb5bd' : '#636e72',
inputBg: isDarkMode ? '#2a2a2a' : '#f1f2f6',
button: '#0984e3',
buttonDisabled: '#74b9ff',
backText: '#0984e3',
}), [isDarkMode]);
const router = useRouter();
const handleSendResetEmail = async () => {
setStatus(null); // Limpa avisos anteriores
if (!email) {
Alert.alert('Atenção', 'Insira seu email');
setStatus({ type: 'error', msg: 'Por favor, insira o seu email corretamente.' });
return;
}
@@ -44,97 +39,272 @@ export default function ForgotPassword() {
const { error } = await supabase.auth.resetPasswordForEmail(email);
if (error) throw error;
Alert.alert('Sucesso!', 'Verifique seu email para redefinir a palavra-passe');
router.back(); // volta para login
setStatus({ type: 'success', msg: 'Link enviado! Verifique ao seu email.' });
// Espera 3 segundos para o utilizador ler e volta para o login
setTimeout(() => router.back(), 3500);
} catch (err: any) {
Alert.alert('Erro', err.message);
setStatus({ type: 'error', msg: 'Não foi possível enviar o email. Tente novamente!' });
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: cores.fundo }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<View style={[styles.container, { backgroundColor: cores.container }]}>
<Text style={[styles.title, { color: cores.texto }]}>Recuperar Palavra-passe</Text>
<Text style={[styles.subtitle, { color: cores.textoSecundario }]}>
Insira seu email para receber o link de redefinição
</Text>
{/* INPUT EMAIL */}
<TextInput
style={[styles.input, { backgroundColor: cores.inputBg, color: cores.texto }]}
placeholder="email@address.com"
placeholderTextColor={isDarkMode ? '#888' : '#a1a1a1'}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
{/* BOTÃO ENVIAR LINK */}
<TouchableOpacity
style={[styles.button, loading && { backgroundColor: cores.buttonDisabled }]}
onPress={handleSendResetEmail}
disabled={loading}
<View style={styles.mainContainer}>
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => router.back()}
style={styles.backButtonTop}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>ENVIAR LINK</Text>}
<View style={styles.backIconCircle}>
<Ionicons name="arrow-back" size={20} color="#1E293B" />
</View>
<Text style={styles.backButtonText}>Voltar</Text>
</TouchableOpacity>
{/* BOTÃO VOLTAR */}
<TouchableOpacity
onPress={() => router.push('/Professor/PerfilProf')}
style={styles.backContainer}
>
<Text style={[styles.backText, { color: cores.backText }]}> Voltar atrás</Text>
</TouchableOpacity>
<View style={styles.content}>
<View style={styles.header}>
<View style={styles.iconWrapper}>
<View style={styles.iconCircle}>
<Ionicons name="mail-unread-outline" size={38} color="#2390a6" />
</View>
<View style={styles.iconBadge} />
</View>
<Text style={styles.title}>Não sabes a tua palavra-passe?</Text>
<Text style={styles.subtitle}>
Não te preocupes! Insere o teu email e enviaremos instruções para recuperares o acesso à nossa app.
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* AVISOS/ERROS MODERNOS AQUI */}
{status && (
<View style={[styles.statusBanner, status.type === 'success' ? styles.successBg : styles.errorBg]}>
<Ionicons
name={status.type === 'success' ? "checkmark-circle" : "alert-circle"}
size={20}
color={status.type === 'success' ? "#059669" : "#EF4444"}
/>
<Text style={[styles.statusText, status.type === 'success' ? styles.successText : styles.errorText]}>
{status.msg}
</Text>
</View>
)}
<View style={styles.form}>
<View style={styles.inputWrapper}>
<Text style={[styles.label, isFocused && { color: '#2390a6' }]}>Email</Text>
<TextInput
style={[
styles.input,
isFocused && styles.inputFocused
]}
placeholder="Insira o seu email"
placeholderTextColor="#94A3B8"
value={email}
onChangeText={(val) => {
setEmail(val);
if(status) setStatus(null); // Limpa o erro ao começar a digitar
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
</View>
<TouchableOpacity
activeOpacity={0.8}
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSendResetEmail}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Enviar Instruções</Text>
)}
</TouchableOpacity>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>EPVC Estágios+ 2026</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
scrollContainer: { flexGrow: 1, justifyContent: 'center', padding: 24 },
container: {
borderRadius: 16,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
mainContainer: {
flex: 1,
backgroundColor: '#FFFFFF',
},
title: { fontSize: 24, fontWeight: '700', marginBottom: 8, textAlign: 'center' },
subtitle: { fontSize: 14, marginBottom: 20, textAlign: 'center' },
input: {
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 16,
scrollContainer: {
flexGrow: 1,
paddingHorizontal: 28,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 20 : 60,
paddingBottom: 40,
},
backButtonTop: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
marginBottom: 30,
},
backIconCircle: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#F8FAFC',
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
borderWidth: 1,
borderColor: '#F1F5F9',
},
backButtonText: {
fontSize: 15,
color: '#64748B',
fontWeight: '600',
},
content: {
flex: 1,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: 35,
},
iconWrapper: {
position: 'relative',
marginBottom: 20,
borderWidth: 0,
},
iconCircle: {
width: 90,
height: 90,
backgroundColor: '#F0F7FF',
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: '#DBEAFE',
},
iconBadge: {
position: 'absolute',
bottom: -4,
right: -4,
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: '#2390a6',
borderWidth: 3,
borderColor: '#FFFFFF',
},
title: {
fontSize: 28,
fontWeight: '900',
color: '#0F172A',
textAlign: 'center',
letterSpacing: -1,
},
subtitle: {
fontSize: 15,
color: '#64748B',
textAlign: 'center',
marginTop: 10,
lineHeight: 22,
maxWidth: 300,
},
// ESTILOS DOS BANNERS MODERNOS
statusBanner: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 16,
marginBottom: 25,
borderWidth: 1,
},
statusText: {
fontSize: 14,
fontWeight: '600',
marginLeft: 10,
flex: 1,
},
errorBg: { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' },
errorText: { color: '#B91C1C' },
successBg: { backgroundColor: '#F0FDF4', borderColor: '#DCFCE7' },
successText: { color: '#166534' },
form: {
width: '100%',
},
inputWrapper: {
marginBottom: 24,
},
label: {
fontSize: 14,
fontWeight: '700',
color: '#475569',
marginBottom: 10,
marginLeft: 4,
},
input: {
backgroundColor: '#FBFDFF',
borderRadius: 18,
paddingHorizontal: 20,
paddingVertical: 18,
fontSize: 16,
color: '#0F172A',
borderWidth: 1.5,
borderColor: '#F1F5F9',
},
inputFocused: {
borderColor: '#2390a6',
backgroundColor: '#FFFFFF',
},
button: {
backgroundColor: '#0984e3',
borderRadius: 12,
paddingVertical: 16,
backgroundColor: '#dd8707',
borderRadius: 18,
paddingVertical: 20,
alignItems: 'center',
marginBottom: 12,
shadowColor: '#0984e3',
shadowOffset: { width: 0, height: 4 },
shadowColor: '#dd8707',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 3,
shadowRadius: 10,
elevation: 6,
},
buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' },
backContainer: { marginTop: 8, alignItems: 'center' },
backText: { fontSize: 15, fontWeight: '500' },
});
buttonDisabled: {
backgroundColor: '#E2E8F0',
elevation: 0,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '800',
},
footer: {
marginTop: 40,
alignItems: 'center',
},
footerText: {
fontSize: 12,
color: '#CBD5E1',
fontWeight: '600',
},
});

View File

@@ -1,12 +1,14 @@
import { useRouter } from 'expo-router';
import {
Alert,
Image,
KeyboardAvoidingView,
Platform,
ScrollView,
StatusBar,
StyleSheet,
Text,
View,
View
} from 'react-native';
import Auth from '../components/Auth';
import { supabase } from './lib/supabase';
@@ -16,7 +18,6 @@ export default function LoginScreen() {
const handleLoginSuccess = async () => {
try {
// Buscar utilizador autenticado
const {
data: { user },
error: userError,
@@ -27,7 +28,6 @@ export default function LoginScreen() {
return;
}
// Buscar perfil
const { data, error } = await supabase
.from('profiles')
.select('tipo')
@@ -44,68 +44,136 @@ export default function LoginScreen() {
return;
}
// Redirecionar
if (data.tipo === 'professor') {
router.replace('/Professor/ProfessorHome');
} else if (data.tipo === 'aluno') {
router.replace('/Aluno/AlunoHome');
} else {
Alert.alert('Erro', 'Tipo inválido');
Alert.alert('Erro', 'Tipo de conta inválido');
}
} catch (err) {
Alert.alert('Erro', 'Erro inesperado no login');
Alert.alert('Erro', 'Ocorreu um erro inesperado no login');
}
};
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: '#f8f9fa' }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
<View style={styles.mainContainer}>
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>📱 Estágios+</Text>
<Text style={styles.subtitle}>
Escola Profissional de Vila do Conde
</Text>
</View>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={styles.content}>
<View style={styles.header}>
{/* Logo Ajustado e Aproximado */}
<View style={styles.logoContainer}>
<Image
source={require('../assets/images/logo_s/texto.png')}
style={styles.logoImage}
resizeMode="contain"
/>
</View>
<Auth onLoginSuccess={handleLoginSuccess} />
</View>
</ScrollView>
</KeyboardAvoidingView>
<Text style={styles.title}>
Estágios<Text style={styles.titlePlus}>+</Text>
</Text>
<Text style={styles.subtitle}>
Escola Profissional de Vila do Conde
</Text>
</View>
<View style={styles.authWrapper}>
<Auth onLoginSuccess={handleLoginSuccess} />
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>Versão 2.1.0</Text>
<View style={styles.footerDivider} />
<Text style={styles.footerText}>© 2026 EPVC</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: '#FFFFFF',
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: 24,
paddingVertical: 40,
paddingHorizontal: 32,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 40 : 60,
paddingBottom: 30,
},
content: {
flex: 1,
justifyContent: 'center',
marginBottom: 40,
},
header: {
alignItems: 'center',
marginBottom: 48,
marginBottom: 20, // Reduzi para o bloco de texto ficar mais perto do Auth
},
logoContainer: {
width: 420,
height: 220,
justifyContent: 'center',
alignItems: 'center',
marginBottom: -60, // MARGEM NEGATIVA: Aproxima o texto da imagem ao máximo
},
logoImage: {
width: '100%',
height: '100%',
},
title: {
fontSize: 32,
fontWeight: '800',
color: '#2d3436',
marginBottom: 8,
fontSize: 36,
fontWeight: '900',
color: '#2390a6',
letterSpacing: -1.5,
},
titlePlus: {
color: '#eb9800',
},
subtitle: {
fontSize: 16,
color: '#636e72',
color: '#64748B',
textAlign: 'center',
marginTop: 2, // Reduzi para o subtítulo ficar colado ao título principal
fontWeight: '500',
lineHeight: 24,
maxWidth: 280,
},
authWrapper: {
width: '100%',
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginTop: 'auto',
paddingTop: 20,
},
footerText: {
fontSize: 13,
color: '#94A3B8',
fontWeight: '600',
},
footerDivider: {
width: 4,
height: 4,
backgroundColor: '#E2E8F0',
borderRadius: 2,
marginHorizontal: 10,
},
});

View File

@@ -1,28 +1,36 @@
// app/forgot-password.tsx
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
View,
} from 'react-native';
import { supabase } from '../app/lib/supabase';
export default function ForgotPassword() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [isFocused, setIsFocused] = useState(false);
// ESTADOS PARA OS AVISOS MODERNOS
const [status, setStatus] = useState<{ type: 'error' | 'success'; msg: string } | null>(null);
const router = useRouter();
const handleSendResetEmail = async () => {
setStatus(null); // Limpa avisos anteriores
if (!email) {
Alert.alert('Atenção', 'Insira seu email');
setStatus({ type: 'error', msg: 'Por favor, insira o seu email corretamente.' });
return;
}
@@ -31,101 +39,272 @@ export default function ForgotPassword() {
const { error } = await supabase.auth.resetPasswordForEmail(email);
if (error) throw error;
Alert.alert('Sucesso!', 'Verifique seu email para redefinir a palavra-passe');
router.back(); // volta para login
setStatus({ type: 'success', msg: 'Link enviado! Verifique ao seu email.' });
// Espera 3 segundos para o utilizador ler e volta para o login
setTimeout(() => router.back(), 3500);
} catch (err: any) {
Alert.alert('Erro', err.message);
setStatus({ type: 'error', msg: 'Não foi possível enviar o email. Tente novamente!' });
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: '#f8f9fa' }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<View style={styles.container}>
<Text style={styles.title}>Recuperar Palavra-passe</Text>
<Text style={styles.subtitle}>Insira seu email para receber o link de redefinição</Text>
{/* INPUT EMAIL */}
<TextInput
style={styles.input}
placeholder="email@address.com"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
{/* BOTÃO ENVIAR LINK */}
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSendResetEmail}
disabled={loading}
<View style={styles.mainContainer}>
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => router.back()}
style={styles.backButtonTop}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>ENVIAR LINK</Text>}
<View style={styles.backIconCircle}>
<Ionicons name="arrow-back" size={20} color="#1E293B" />
</View>
<Text style={styles.backButtonText}>Voltar</Text>
</TouchableOpacity>
{/* BOTÃO VOLTAR */}
<TouchableOpacity onPress={() => router.push('/')} style={styles.backContainer}>
<Text style={styles.backText}> Voltar para Login</Text>
</TouchableOpacity>
<View style={styles.content}>
<View style={styles.header}>
<View style={styles.iconWrapper}>
<View style={styles.iconCircle}>
<Ionicons name="mail-unread-outline" size={38} color="#2390a6" />
</View>
<View style={styles.iconBadge} />
</View>
<Text style={styles.title}>Não sabes a tua palavra-passe?</Text>
<Text style={styles.subtitle}>
Não te preocupes! Insere o teu email e enviaremos instruções para recuperares o acesso à nossa app.
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* AVISOS/ERROS MODERNOS AQUI */}
{status && (
<View style={[styles.statusBanner, status.type === 'success' ? styles.successBg : styles.errorBg]}>
<Ionicons
name={status.type === 'success' ? "checkmark-circle" : "alert-circle"}
size={20}
color={status.type === 'success' ? "#059669" : "#EF4444"}
/>
<Text style={[styles.statusText, status.type === 'success' ? styles.successText : styles.errorText]}>
{status.msg}
</Text>
</View>
)}
<View style={styles.form}>
<View style={styles.inputWrapper}>
<Text style={[styles.label, isFocused && { color: '#2390a6' }]}>Email</Text>
<TextInput
style={[
styles.input,
isFocused && styles.inputFocused
]}
placeholder="Insira o seu email"
placeholderTextColor="#94A3B8"
value={email}
onChangeText={(val) => {
setEmail(val);
if(status) setStatus(null); // Limpa o erro ao começar a digitar
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
</View>
<TouchableOpacity
activeOpacity={0.8}
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSendResetEmail}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Enviar Instruções</Text>
)}
</TouchableOpacity>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>EPVC Estágios+ 2026</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
scrollContainer: { flexGrow: 1, justifyContent: 'center', padding: 24 },
container: {
backgroundColor: '#fff',
mainContainer: {
flex: 1,
backgroundColor: '#FFFFFF',
},
scrollContainer: {
flexGrow: 1,
paddingHorizontal: 28,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) + 20 : 60,
paddingBottom: 40,
},
backButtonTop: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
marginBottom: 30,
},
backIconCircle: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#F8FAFC',
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
borderWidth: 1,
borderColor: '#F1F5F9',
},
backButtonText: {
fontSize: 15,
color: '#64748B',
fontWeight: '600',
},
content: {
flex: 1,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: 35,
},
iconWrapper: {
position: 'relative',
marginBottom: 20,
},
iconCircle: {
width: 90,
height: 90,
backgroundColor: '#F0F7FF',
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: '#DBEAFE',
},
iconBadge: {
position: 'absolute',
bottom: -4,
right: -4,
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: '#2390a6',
borderWidth: 3,
borderColor: '#FFFFFF',
},
title: {
fontSize: 28,
fontWeight: '900',
color: '#0F172A',
textAlign: 'center',
letterSpacing: -1,
},
subtitle: {
fontSize: 15,
color: '#64748B',
textAlign: 'center',
marginTop: 10,
lineHeight: 22,
maxWidth: 300,
},
// ESTILOS DOS BANNERS MODERNOS
statusBanner: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 16,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
marginBottom: 25,
borderWidth: 1,
},
logo: {
width: 120,
height: 120,
alignSelf: 'center',
marginBottom: 20,
statusText: {
fontSize: 14,
fontWeight: '600',
marginLeft: 10,
flex: 1,
},
errorBg: { backgroundColor: '#FEF2F2', borderColor: '#FEE2E2' },
errorText: { color: '#B91C1C' },
successBg: { backgroundColor: '#F0FDF4', borderColor: '#DCFCE7' },
successText: { color: '#166534' },
form: {
width: '100%',
},
inputWrapper: {
marginBottom: 24,
},
label: {
fontSize: 14,
fontWeight: '700',
color: '#475569',
marginBottom: 10,
marginLeft: 4,
},
title: { fontSize: 24, fontWeight: '700', color: '#2d3436', marginBottom: 8, textAlign: 'center' },
subtitle: { fontSize: 14, color: '#636e72', marginBottom: 20, textAlign: 'center' },
input: {
backgroundColor: '#f1f2f6',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: '#FBFDFF',
borderRadius: 18,
paddingHorizontal: 20,
paddingVertical: 18,
fontSize: 16,
marginBottom: 20,
borderWidth: 0,
color: '#2d3436',
color: '#0F172A',
borderWidth: 1.5,
borderColor: '#F1F5F9',
},
inputFocused: {
borderColor: '#2390a6',
backgroundColor: '#FFFFFF',
},
button: {
backgroundColor: '#0984e3',
borderRadius: 12,
paddingVertical: 16,
backgroundColor: '#dd8707',
borderRadius: 18,
paddingVertical: 20,
alignItems: 'center',
marginBottom: 12,
shadowColor: '#0984e3',
shadowOffset: { width: 0, height: 4 },
shadowColor: '#dd8707',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 3,
shadowRadius: 10,
elevation: 6,
},
buttonDisabled: { backgroundColor: '#74b9ff' },
buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' },
backContainer: { marginTop: 8, alignItems: 'center' },
backText: { color: '#0984e3', fontSize: 15, fontWeight: '500' },
});
buttonDisabled: {
backgroundColor: '#E2E8F0',
elevation: 0,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '800',
},
footer: {
marginTop: 40,
alignItems: 'center',
},
footerText: {
fontSize: 12,
color: '#CBD5E1',
fontWeight: '600',
},
});