ATUALIZACOES
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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 }]}>Nº 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 }]}>Nº 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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' }
|
||||
});
|
||||
@@ -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 }
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
134
app/index.tsx
134
app/index.tsx
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 172 KiB |
BIN
assets/images/logo_s/texto.png
Normal file
BIN
assets/images/logo_s/texto.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -1,6 +1,15 @@
|
||||
// components/Auth.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { ActivityIndicator, Alert, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../app/lib/supabase';
|
||||
|
||||
interface AuthProps {
|
||||
@@ -11,96 +20,206 @@ export default function Auth({ onLoginSuccess }: AuthProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState<string | null>(null);
|
||||
|
||||
// ESTADO PARA OS AVISOS MODERNOS
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async () => {
|
||||
setErrorMessage(null); // Limpa erros anteriores
|
||||
|
||||
if (!email || !password) {
|
||||
Alert.alert('Atenção', 'Preencha todos os campos');
|
||||
setErrorMessage('Preencha todos os campos para continuar.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. LOGIN NO AUTH DO SUPABASE
|
||||
const { data: { user }, error: authError } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (authError) throw authError;
|
||||
|
||||
// 2. BUSCAR DADOS NA TABELA PROFILES LOGO APÓS O LOGIN
|
||||
if (user) {
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (profileError) {
|
||||
console.warn("Perfil não encontrado na tabela profiles.");
|
||||
} else {
|
||||
console.log("Perfil carregado com sucesso:", profile.nome);
|
||||
}
|
||||
if (profileError) console.warn("Perfil não encontrado.");
|
||||
}
|
||||
|
||||
Alert.alert('Bem-vindo(a)!');
|
||||
|
||||
// 3. SE SUCESSO, EXECUTA O CALLBACK E NAVEGA
|
||||
if (onLoginSuccess) {
|
||||
onLoginSuccess();
|
||||
} else {
|
||||
router.replace('/(tabs)/estagios'); // Caminho padrão caso não venha callback
|
||||
router.replace('/(tabs)/estagios');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
Alert.alert('Erro', err.message);
|
||||
// Tradução simples de erro comum
|
||||
const msg = err.message === 'Invalid login credentials'
|
||||
? 'Email ou palavra-passe incorretos.'
|
||||
: 'Ocorreu um erro ao entrar. Tenta novamente.';
|
||||
setErrorMessage(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.form}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="email@address.com"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
editable={!loading}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
|
||||
{/* AVISO DE ERRO MODERNO */}
|
||||
{errorMessage && (
|
||||
<View style={styles.errorBanner}>
|
||||
<Ionicons name="alert-circle" size={20} color="#EF4444" />
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text style={styles.label}>Palavra-passe</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira a sua palavra-passe"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.label, isFocused === 'email' && { color: '#2390a6' }]}>Email</Text>
|
||||
<TextInput
|
||||
style={[styles.input, isFocused === 'email' && styles.inputFocused]}
|
||||
placeholder="Insira o seu email"
|
||||
placeholderTextColor="#94A3B8"
|
||||
value={email}
|
||||
onChangeText={(val) => {
|
||||
setEmail(val);
|
||||
if(errorMessage) setErrorMessage(null);
|
||||
}}
|
||||
onFocus={() => setIsFocused('email')}
|
||||
onBlur={() => setIsFocused(null)}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputWrapper}>
|
||||
<View style={styles.labelRow}>
|
||||
<Text style={[styles.label, isFocused === 'pass' && { color: '#2390a6' }]}>Palavra-passe</Text>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, isFocused === 'pass' && styles.inputFocused]}
|
||||
placeholder="Insira a sua palavra-passe"
|
||||
placeholderTextColor="#94A3B8"
|
||||
value={password}
|
||||
onChangeText={(val) => {
|
||||
setPassword(val);
|
||||
if(errorMessage) setErrorMessage(null);
|
||||
}}
|
||||
onFocus={() => setIsFocused('pass')}
|
||||
onBlur={() => setIsFocused(null)}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>ENTRAR</Text>}
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>ENTRAR</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => router.push('/redefenirsenha')}>
|
||||
<Text style={styles.forgotText}>Esqueceu a palavra-passe?</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.forgotButton}
|
||||
onPress={() => router.push('/redefenirsenha')}
|
||||
>
|
||||
<Text style={styles.forgotText}>Esqueceu-se da palavra-passe?</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ... (teus estilos mantêm-se iguais)
|
||||
const styles = StyleSheet.create({
|
||||
form: { backgroundColor: '#fff', borderRadius: 16, padding: 24, marginTop: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 5 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: '#2d3436', marginBottom: 8 },
|
||||
input: { backgroundColor: '#f1f2f6', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, marginBottom: 20, borderWidth: 0, color: '#2d3436' },
|
||||
button: { backgroundColor: '#0984e3', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginBottom: 12, shadowColor: '#0984e3', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 3 },
|
||||
buttonDisabled: { backgroundColor: '#74b9ff' },
|
||||
buttonText: { color: '#fff', fontSize: 17, fontWeight: '700' },
|
||||
forgotText: { color: '#0984e3', fontSize: 15, fontWeight: '500', textAlign: 'center', marginTop: 8 },
|
||||
container: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
},
|
||||
errorBanner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FEF2F2',
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
marginBottom: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#FEE2E2',
|
||||
},
|
||||
errorText: {
|
||||
color: '#B91C1C',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 10,
|
||||
flex: 1,
|
||||
},
|
||||
inputWrapper: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
labelRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#475569',
|
||||
marginBottom: 10,
|
||||
marginLeft: 4,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#FBFDFF',
|
||||
borderRadius: 18,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
fontSize: 16,
|
||||
color: '#0F172A',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#F1F5F9',
|
||||
},
|
||||
inputFocused: {
|
||||
borderColor: '#2390a6',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#dd8707', // A cor que pediste
|
||||
borderRadius: 18,
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
shadowColor: '#dd8707',
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 10,
|
||||
elevation: 6,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#E2E8F0',
|
||||
elevation: 0,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
forgotButton: {
|
||||
marginTop: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
forgotText: {
|
||||
color: '#2390a6',
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-linear-gradient": "~15.0.8",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-router": "~6.0.17",
|
||||
@@ -6582,6 +6583,17 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-linear-gradient": {
|
||||
"version": "15.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.8.tgz",
|
||||
"integrity": "sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-linking": {
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.10.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-linear-gradient": "~15.0.8",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-router": "~6.0.17",
|
||||
|
||||
Reference in New Issue
Block a user