diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index 81cecbd..4e9ad8d 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -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>({}); const [faltas, setFaltas] = useState>({}); const [sumarios, setSumarios] = useState>({}); const [urlsJustificacao, setUrlsJustificacao] = useState>({}); + // Estados de UI const [pdf, setPdf] = useState(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 = {}; - const objFaltas: Record = {}; - const objSumarios: Record = {}; - const objUrls: Record = {}; - + 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 ( + + {/* ALERT OVERLAY MODERNO */} + {alertConfig && ( + + + {alertConfig.msg} + + )} + - router.push('/Aluno/perfil')}> @@ -301,9 +229,9 @@ const AlunoHome = memo(() => { {isLocating ? : Marcar Presença} @@ -312,84 +240,67 @@ const AlunoHome = memo(() => { onPress={handleFalta} disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]} > - {infoData.ehFuturo ? "Vou Faltar" : "Faltei"} + Faltei - {isLoadingDB && ( - - - - )} + {isLoadingDB && } ({ ...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); }} /> - {infoData.nomeFeriado && ( - - - {infoData.nomeFeriado} - - )} - {presencas[selectedDate] && ( - Sumário do Dia - {!editandoSumario && ( - setEditandoSumario(true)}> - - - )} + Sumário + {!editandoSumario && setEditandoSumario(true)}>} 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 && ( - - Guardar Sumário - - )} + {editandoSumario && Guardar} )} {faltas[selectedDate] && ( - Justificação de Falta + Justificação {urlsJustificacao[selectedDate] ? ( visualizarDocumento(urlsJustificacao[selectedDate])}> - Ver Justificação (PDF) + Ver PDF ) : ( - + { + const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' }); + if (!res.canceled) setPdf(res.assets[0]); + }}> {pdf ? pdf.name : 'Selecionar PDF'} - - {isUploading ? : Enviar para o Servidor} - + {pdf && {isUploading ? : Enviar}} )} @@ -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; \ No newline at end of file diff --git a/app/Professor/Alunos/DetalhesAluno.tsx b/app/Professor/Alunos/DetalhesAluno.tsx index 21ccfad..2643e53 100644 --- a/app/Professor/Alunos/DetalhesAluno.tsx +++ b/app/Professor/Alunos/DetalhesAluno.tsx @@ -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(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 ( @@ -128,115 +134,128 @@ const DetalhesAlunos = memo(() => { ); } - // Verificação de segurança extra para o objeto aluno - if (!aluno) { - return ( - - Dados indisponíveis - - ); - } - return ( - + - - router.back()} style={[styles.backBtn, { backgroundColor: cores.card }]}> - - - Ficha do Aluno - - - - + - - - {aluno.nome.charAt(0).toUpperCase()} - - {aluno.nome} - - {aluno.turma_curso} - - - - Dados Pessoais - - - - - - - - Informação de Estágio - - - + {/* HEADER */} + + router.back()} + > + + - - - - - - Horários Registados - {/* O USO DE OPTIONAL CHAINING AQUI EVITA O ERRO */} - {(aluno.horarios_detalhados?.length ?? 0) > 0 ? ( - aluno.horarios_detalhados.map((h, index) => ( - {h} - )) - ) : ( - Sem horários definidos - )} - - - - - - - - - - + + Ficha Aluno + + Detalhes acadêmicos e estágio + - - + + {/* SECÇÃO PERFIL */} + + + {aluno?.nome.charAt(0).toUpperCase()} + + + {aluno?.nome} + {aluno?.turma_curso} + + + + {/* DADOS PESSOAIS */} + + Dados Pessoais + + + + + + + + + + + {/* INFORMAÇÃO DE ESTÁGIO */} + + Estágio Atual + + + + + + + + + + + Horários Registados + {aluno?.horarios_detalhados && aluno.horarios_detalhados.length > 0 ? ( + aluno.horarios_detalhados.map((h, i) => ( + {h} + )) + ) : ( + Não definidos + )} + + + + + + + + + + + + + + + ); }); -const InfoRow = ({ icon, label, valor, cores, ultimo }: any) => ( - - - - +const DetailRow = ({ icon, label, value, cores, ultimo }: any) => ( + + - {label} - {valor} + {label} + {value} ); -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' }, -}); \ No newline at end of file + 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; \ No newline at end of file diff --git a/app/Professor/Alunos/Estagios.tsx b/app/Professor/Alunos/Estagios.tsx index 5d69f8e..646e320 100644 --- a/app/Professor/Alunos/Estagios.tsx +++ b/app/Professor/Alunos/Estagios.tsx @@ -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([]); const [alunos, setAlunos] = useState([]); const [empresas, setEmpresas] = useState([]); 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(null); const [empresaSelecionada, setEmpresaSelecionada] = useState(null); const [editandoEstagio, setEditandoEstagio] = useState(null); - const [dataInicio, setDataInicio] = useState(''); const [dataFim, setDataFim] = useState(''); - const [horarios, setHorarios] = useState([]); - 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 = {}; - 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 = {}; - 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 = {}; 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 ; + const alunosAgrupados = useMemo(() => { + const groups: Record = {}; + 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 = {}; + empresas.forEach(e => { + const curso = (e.curso || 'Geral').trim().toUpperCase(); + if (!groups[curso]) groups[curso] = []; + groups[curso].push(e); + }); + return groups; + }, [empresas]); return ( - + + {/* Header */} - router.back()} - > - + router.back()}> + - Estágios - + Estágios + - - - - + + + + - {estagiosAgrupados.map(grupo => ( - - - - {grupo.titulo} - + {estagiosFiltrados.map(grupo => ( + + + + {grupo.titulo} {grupo.dados.map(e => ( - - { - 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); - }}> - {e.alunos?.nome} - - - {e.alunos?.turma_curso} - - ⏱ {e.horas_diarias || '0h'}/dia - - 🏢 {e.empresas?.nome} - - + 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)} > - + + + {e.alunos?.nome.charAt(0)} + + + {e.alunos?.nome} + {e.alunos?.turma_curso} + + + + + {e.empresas?.nome} + {e.horas_diarias}/dia + ))} ))} - - { - setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); - setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true); - }} - > - - NOVO ESTÁGIO - + + { setPasso(1); setModalVisible(true); }}> + + Novo Estágio + + {/* --- Modal Principal (Cadastro/Edição) --- */} - - + + + - - - {passo === 1 ? "Seleção de Aluno/Empresa" : "Configuração de Estágio"} - - {passo === 2 && {alunoSelecionado?.nome}} - - - - + {passo === 1 ? "Vincular Dados" : "Detalhes do Estágio"} + - + {passo === 1 ? ( - - SELECIONE O ALUNO - - - {Object.keys(alunosAgrupados).map(t => ( - - {t} - {alunosAgrupados[t].map(a => ( - setAlunoSelecionado(a)} - > - - {a.nome} - - {alunoSelecionado?.id === a.id && } + + Estagiário + + + {Object.entries(alunosAgrupados).map(([turma, lista]) => ( + + {turma} + {lista.map(a => ( + setAlunoSelecionado(a)}> + {a.nome} ))} @@ -341,22 +304,15 @@ export default function Estagios() { - SELECIONE A EMPRESA - - - {Object.keys(empresasAgrupadas).sort().map(curso => ( + Empresa + + + {Object.entries(empresasAgrupadas).map(([curso, lista]) => ( - {curso.toUpperCase()} - {empresasAgrupadas[curso].map(emp => ( - setEmpresaSelecionada(emp)} - > - - {emp.nome} - - {empresaSelecionada?.id === emp.id && } + {curso} + {lista.map(emp => ( + setEmpresaSelecionada(emp)}> + {emp.nome} ))} @@ -365,73 +321,74 @@ export default function Estagios() { ) : ( - - - DURAÇÃO - - - INÍCIO - - - - FIM - - - - - Total: {totalHorasDiarias} - - - - - - HORÁRIOS - setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}> - - + + + Duração + + INÍCIO + FIM - {horarios.map((h, i) => ( - - { const n = [...horarios]; n[i].periodo = t; setHorarios(n); }} /> - { const n = [...horarios]; n[i].hora_inicio = t; setHorarios(n); }} /> - ATÉ - { const n = [...horarios]; n[i].hora_fim = t; setHorarios(n); }} /> - setHorarios(horarios.filter((_, idx) => idx !== i))}> - - - - ))} - - DADOS DO TUTOR - setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome do Tutor"/> - setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/> + + Horários + + Manhã + + + + + Tarde + + + + Total: {totalHorasDiarias}/dia + + + + Tutor + setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholder="Nome do Tutor"/> + setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Contacto"/> )} - {passo === 2 && !editandoEstagio && ( - setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.azulSuave }]}> - VOLTAR - - )} + {passo === 2 && setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.fundo }]}>VOLTAR} { 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 }]} > - - {passo === 1 ? "PRÓXIMO" : "GRAVAR"} - + {passo === 1 ? "PRÓXIMO" : "FINALIZAR"} + + + + + + + {/* --- NOVO MODAL DE ELIMINAÇÃO MODERNIZADO --- */} + + + + + + + Eliminar Estágio? + + Estás prestes a apagar o registo de {estagioParaApagar?.nome}. Esta ação é irreversível. + + + setDeleteModalVisible(false)}> + CANCELAR + + + ELIMINAR @@ -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' }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/ListaAlunos.tsx b/app/Professor/Alunos/ListaAlunos.tsx index 086f444..89495d8 100644 --- a/app/Professor/Alunos/ListaAlunos.tsx +++ b/app/Professor/Alunos/ListaAlunos.tsx @@ -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([]); 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 = {}; 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 ( - - + + - {/* HEADER FIXO */} - - + + + {/* HEADER IGUAL ÀS EMPRESAS */} + router.back()} - style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]} + onPress={() => router.back()} > - + + + + + Alunos + + Gestão de turmas e estágios + + + + + - Alunos - - - - + {/* SEARCH BAR IGUAL ÀS EMPRESAS */} + + + + + - - {loading ? ( - - - - ) : ( - item.nome} - contentContainerStyle={styles.scrollContent} - showsVerticalScrollIndicator={false} - renderItem={({ item }) => ( - - - - {item.nome} • {item.alunos.length} Alunos - + {loading && !refreshing ? ( + + + + ) : ( + item.nome} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + refreshControl={} + renderItem={({ item }) => ( + + {/* Cabeçalho da Secção (Turma) */} + + {item.nome} + + + + {item.alunos.map((aluno) => ( + router.push({ + pathname: '/Professor/Alunos/DetalhesAluno', + params: { alunoId: aluno.id } + })} + > + + + + + + {aluno.nome} + + + Nº Escola: {aluno.n_escola} + + + + + + ))} - - {item.alunos.map(aluno => ( - - router.push({ - pathname: '/Professor/Alunos/DetalhesAluno', - params: { alunoId: aluno.id }, - }) - } - > - - - {aluno.nome.charAt(0).toUpperCase()} - - - - {aluno.nome} - Nº Escola: {aluno.n_escola} - - - - ))} - - )} - /> - )} - + )} + ListEmptyComponent={() => ( + + + Nenhum aluno encontrado. + + )} + /> + )} + + ); }); -// --- 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; \ No newline at end of file diff --git a/app/Professor/Empresas/DetalhesEmpresa.tsx b/app/Professor/Empresas/DetalhesEmpresa.tsx index 26bb365..2983862 100644 --- a/app/Professor/Empresas/DetalhesEmpresa.tsx +++ b/app/Professor/Empresas/DetalhesEmpresa.tsx @@ -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({ ...empresaOriginal }); - const [alunos, setAlunos] = useState([]); + 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 ( - - + + + + {/* FEEDBACK TOAST */} + {showSuccess && ( + + + Alterações guardadas com sucesso! + + )} - + {/* MODAL DE APAGAR MODERNO */} + + + + + + + Tens a certeza? + + Esta ação irá remover permanentemente a entidade {empresaLocal.nome}. + + + + setShowDeleteModal(false)} + > + Cancelar + + + + {loading ? : Apagar} + + + + + + + - router.back()}> - + router.back()} + > + - - Detalhes da Entidade - + Detalhes da Entidade setEditando(!editando)} + style={[styles.btnAction, { backgroundColor: editando ? cores.vermelho : cores.card, borderColor: editando ? cores.vermelho : cores.borda }]} + onPress={() => { + if(editando) setEmpresaLocal({...empresaOriginal}); + setEditando(!editando); + }} > - + - - Informação da Empresa - setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} /> - setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} /> - setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} /> + + + + Dados Gerais + + setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} /> + setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} /> + setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline /> - - Contacto do Tutor - setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} /> - setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" /> + + + + Contacto Tutor + + setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} /> + setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" /> - - - Alunos em Estágio + + + + Alunos Ativos - - {loadingAlunos ? '...' : alunos.length} - + {loadingAlunos ? '...' : alunos.length} - - {loadingAlunos ? ( - - ) : alunos.length > 0 ? ( - alunos.map((aluno, index) => ( - - - {aluno.nome.charAt(0).toUpperCase()} + {loadingAlunos ? : alunos.length > 0 ? ( + alunos.map((aluno, i) => ( + + + {aluno.nome.charAt(0).toUpperCase()} - {aluno.nome} + {aluno.nome} )) - ) : ( - - - Nenhum aluno associado - - )} + ) : Sem alunos vinculados.} - {editando ? ( - - {loading ? : Guardar Alterações} - - ) : ( - - - Remover Entidade - - )} - + + {editando ? ( + + {loading ? : ( + Guardar Alterações + )} + + ) : ( + setShowDeleteModal(true)}> + + Eliminar Entidade + + )} + - + ); }); -const InfoItem = ({ label, value, icon, editable, onChangeText, cores, ...props }: InfoItemProps) => ( - - - - - - {label} - {editable ? ( - - ) : ( - {value || '---'} - )} - +const ModernField = ({ label, value, editable, cores, ...props }: any) => ( + + {label} + {editable ? ( + + ) : ( + + {value || '---'} + + )} ); 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; \ No newline at end of file diff --git a/app/Professor/Empresas/ListaEmpresas.tsx b/app/Professor/Empresas/ListaEmpresas.tsx index 78b3a97..750938a 100644 --- a/app/Professor/Empresas/ListaEmpresas.tsx +++ b/app/Professor/Empresas/ListaEmpresas.tsx @@ -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([]); 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 ( - + - + - {/* HEADER */} + {/* HEADER MODERNIZADO */} router.back()} > - + Empresas - {empresas.length} entidades no total + {empresas.length} entidades ativas @@ -175,17 +159,17 @@ const ListaEmpresasProfessor = memo(() => { style={[styles.addBtn, { backgroundColor: cores.azul }]} onPress={() => setModalVisible(true)} > - + {/* SEARCH BAR */} - + { - {/* CONTEÚDO / LISTA */} {loading && !refreshing ? ( @@ -203,29 +186,19 @@ const ListaEmpresasProfessor = memo(() => { sections={secoesAgrupadas} keyExtractor={item => item.id.toString()} stickySectionHeadersEnabled={false} - contentContainerStyle={[ - styles.listPadding, - { paddingBottom: insets.bottom + 100 } - ]} - refreshControl={ - - } + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + refreshControl={} renderSectionHeader={({ section: { title } }) => ( - {title} + )} renderItem={({ item }) => ( - 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) } })} > @@ -234,60 +207,50 @@ const ListaEmpresasProfessor = memo(() => { {item.nome} - + {item.tutor_nome} - + )} ListEmptyComponent={() => ( - - Nenhuma empresa encontrada. + + Sem resultados. )} /> )} - {/* MODAL DE CRIAÇÃO */} + {/* MODAL DE CRIAÇÃO PREMIUM */} - - + + - Registar Empresa - setModalVisible(false)}> - + + Nova Empresa + Preenche os detalhes da entidade + + setModalVisible(false)} style={styles.closeBtn}> + - - - - - - + + setForm({...form, nome:v})} cores={cores} /> + setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI" /> + setForm({...form, morada:v})} cores={cores} /> - - Guardar Empresa + + + setForm({...form, tutorNome:v})} cores={cores} /> + setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} /> + + + Registar Entidade @@ -301,48 +264,47 @@ const ModernInput = ({ label, icon, cores, ...props }: any) => ( {label} - - + + ); 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; \ No newline at end of file diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index a1247f6..fb7a2e5 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -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(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() { + {/* BANNER DE AVISO */} + {alertConfig && ( + + + {alertConfig.msg} + + )} + {/* HEADER */} @@ -139,7 +170,7 @@ export default function PerfilProfessor() { {perfil?.nome} - {perfil?.curso || 'Professor'} + {perfil?.curso || 'Professor / Tutor'} @@ -206,7 +237,7 @@ export default function PerfilProfessor() { Alterar Palavra-passe - + ( { backgroundColor: cores.fundo, borderColor: editable ? cores.azul : cores.borda, - opacity: editable ? 1 : 0.7 + opacity: editable ? 1 : 0.8 } ]}> @@ -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' } }); \ No newline at end of file diff --git a/app/Professor/ProfessorHome.tsx b/app/Professor/ProfessorHome.tsx index 7707851..0d08459 100644 --- a/app/Professor/ProfessorHome.tsx +++ b/app/Professor/ProfessorHome.tsx @@ -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(''); 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 ( - - + // Aplicamos as edges para garantir que o fundo cobre tudo mas o conteúdo respeita os limites + + - {/* HEADER COM NOME COMPLETO */} + + {/* HEADER */} - Bem-vindo, - {loading ? ( - - ) : ( - - {nome || 'Professor'} - - )} + + + Bem-vindo, + {loading ? ( + + ) : ( + {nome || 'Professor'} + )} + + router.push('/Professor/PerfilProf')} + style={[styles.avatarMini, { backgroundColor: cores.azul }]} + > + {nome?.charAt(0).toUpperCase()} + + - - - Painel de Gestão de Estágios + + + Painel de Gestão de Estágios - {/* GRID DE OPÇÕES */} + {/* GRID */} - router.push('/Professor/PerfilProf')} - cores={cores} - /> + router.push('/Professor/Alunos/Sumarios')} cores={cores} /> + router.push('/Professor/Alunos/Presencas')} cores={cores} /> + router.push('/Professor/Alunos/Faltas')} cores={cores} /> + router.push('/Professor/Alunos/ListaAlunos')} cores={cores} /> + router.push('/Professor/Alunos/Estagios')} cores={cores} /> + router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} /> + router.push('/Professor/defenicoes2')} cores={cores} /> + router.push('/Professor/PerfilProf')} cores={cores} /> + - router.push('/Professor/defenicoes2')} - cores={cores} - /> - - router.push('/Professor/Alunos/Sumarios')} - cores={cores} - /> - - router.push('/Professor/Alunos/Faltas')} - cores={cores} - /> - - router.push('/Professor/Alunos/Presencas')} - cores={cores} - /> - - router.push('/Professor/Alunos/Estagios')} - cores={cores} - /> - - router.push('/Professor/Alunos/ListaAlunos')} - cores={cores} - /> - - router.push('/Professor/Empresas/ListaEmpresas')} - cores={cores} - /> + + EPVC • Gestão de Estágios ); } -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 ( - + - {title} - {subtitle} + + {title} + {subtitle} + + ); } 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 } }); \ No newline at end of file diff --git a/app/Professor/defenicoes2.tsx b/app/Professor/defenicoes2.tsx index 25bdaaf..be17851 100644 --- a/app/Professor/defenicoes2.tsx +++ b/app/Professor/defenicoes2.tsx @@ -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 ( - - - + + + {/* BANNER DE FEEDBACK */} + {alertConfig && ( + + + {alertConfig.msg} + + )} + + - {/* HEADER */} + {/* HEADER ESTILIZADO */} router.back()} > - + Definições - + - {/* SECÇÃO PREFERÊNCIAS */} - Preferências - + {/* GRUPO: PERSONALIZAÇÃO */} + Personalização + - - + + - Notificações + Notificações Push { + setNotificacoes(v); + showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info"); + }} + trackColor={{ false: '#CBD5E1', true: cores.azul }} + thumbColor="#FFFFFF" /> - - + + - Modo Escuro + Interface Escura - {/* SECÇÃO SUPORTE */} - Suporte e Contactos - - abrirURL('mailto:epvc@epvc.pt')} - > - - - - - Direção - epvc@epvc.pt - - - - - abrirURL('mailto:secretaria@epvc.pt')} - > - - - - - Secretaria - secretaria@epvc.pt - - - - - abrirURL('tel:252641805')} - > - - - - - Telefone - 252 641 805 - - - + {/* GRUPO: CONTACTOS EPVC */} + Escola Profissional + + abrirURL('mailto:epvc@epvc.pt')} + cores={cores} + showBorder + /> + abrirURL('mailto:secretaria@epvc.pt')} + cores={cores} + showBorder + /> + abrirURL('tel:252641805')} + cores={cores} + /> - {/* SECÇÃO INFO & SAIR */} - + {/* GRUPO: SEGURANÇA & INFO */} + - - + + - Versão da App - 26.3.11 + Versão Estável + v2.6.17 - - - + + + - Terminar Sessão - + Terminar Sessão + - Escola Profissional de Vila do Conde © 2026 + + Desenvolvido para PAP • 2026 + EPVC + @@ -177,24 +193,39 @@ const Definicoes = memo(() => { ); }); +// Componente Auxiliar para Links +const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => ( + + + + + + {label} + {subLabel} + + + +); + 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; \ No newline at end of file diff --git a/app/Professor/redefenirsenha2.tsx b/app/Professor/redefenirsenha2.tsx index 7a8ce99..9b086eb 100644 --- a/app/Professor/redefenirsenha2.tsx +++ b/app/Professor/redefenirsenha2.tsx @@ -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 ( - - - - - Recuperar Palavra-passe - - Insira seu email para receber o link de redefinição - - - {/* INPUT EMAIL */} - - - {/* BOTÃO ENVIAR LINK */} - + + + + router.back()} + style={styles.backButtonTop} > - {loading ? : ENVIAR LINK} + + + + Voltar - {/* BOTÃO VOLTAR */} - router.push('/Professor/PerfilProf')} - style={styles.backContainer} - > - ← Voltar atrás - + + + + + + + + + + Não sabes a tua palavra-passe? + + Não te preocupes! Insere o teu email e enviaremos instruções para recuperares o acesso à nossa app. + + - - - + {/* AVISOS/ERROS MODERNOS AQUI */} + {status && ( + + + + {status.msg} + + + )} + + + + Email + { + 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} + /> + + + + {loading ? ( + + ) : ( + Enviar Instruções + )} + + + + + + EPVC Estágios+ • 2026 + + + + ); } 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', + }, +}); \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 57dca80..863fac1 100644 --- a/app/index.tsx +++ b/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 ( - - + + - - - 📱 Estágios+ - - Escola Profissional de Vila do Conde - - + + + + + {/* Logo Ajustado e Aproximado */} + + + - - - - + + Estágios+ + + + Escola Profissional de Vila do Conde + + + + + + + + + + Versão 2.1.0 + + © 2026 EPVC + + + + ); } 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, }, }); \ No newline at end of file diff --git a/app/redefenirsenha.tsx b/app/redefenirsenha.tsx index 7bde524..4494959 100644 --- a/app/redefenirsenha.tsx +++ b/app/redefenirsenha.tsx @@ -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 ( - - - - - Recuperar Palavra-passe - Insira seu email para receber o link de redefinição - - {/* INPUT EMAIL */} - - - {/* BOTÃO ENVIAR LINK */} - + + + + router.back()} + style={styles.backButtonTop} > - {loading ? : ENVIAR LINK} + + + + Voltar - {/* BOTÃO VOLTAR */} - router.push('/')} style={styles.backContainer}> - ← Voltar para Login - + + + + + + + + + + Não sabes a tua palavra-passe? + + Não te preocupes! Insere o teu email e enviaremos instruções para recuperares o acesso à nossa app. + + - - - + {/* AVISOS/ERROS MODERNOS AQUI */} + {status && ( + + + + {status.msg} + + + )} + + + + Email + { + 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} + /> + + + + {loading ? ( + + ) : ( + Enviar Instruções + )} + + + + + + EPVC Estágios+ • 2026 + + + + ); } 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', + }, +}); \ No newline at end of file diff --git a/assets/images/logo.png b/assets/images/logo.png index e69de29..c99bd02 100644 Binary files a/assets/images/logo.png and b/assets/images/logo.png differ diff --git a/assets/images/logo_s/texto.png b/assets/images/logo_s/texto.png new file mode 100644 index 0000000..b059a94 Binary files /dev/null and b/assets/images/logo_s/texto.png differ diff --git a/components/Auth.tsx b/components/Auth.tsx index e774286..a7d2341 100644 --- a/components/Auth.tsx +++ b/components/Auth.tsx @@ -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(null); + + // ESTADO PARA OS AVISOS MODERNOS + const [errorMessage, setErrorMessage] = useState(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 ( - - Email - + + + {/* AVISO DE ERRO MODERNO */} + {errorMessage && ( + + + {errorMessage} + + )} - Palavra-passe - + + Email + { + setEmail(val); + if(errorMessage) setErrorMessage(null); + }} + onFocus={() => setIsFocused('email')} + onBlur={() => setIsFocused(null)} + autoCapitalize="none" + keyboardType="email-address" + editable={!loading} + /> + + + + + Palavra-passe + + { + setPassword(val); + if(errorMessage) setErrorMessage(null); + }} + onFocus={() => setIsFocused('pass')} + onBlur={() => setIsFocused(null)} + secureTextEntry + editable={!loading} + /> + - {loading ? : ENTRAR} + {loading ? ( + + ) : ( + ENTRAR + )} - router.push('/redefenirsenha')}> - Esqueceu a palavra-passe? + router.push('/redefenirsenha')} + > + Esqueceu-se da palavra-passe? ); } -// ... (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', + }, }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index acceb8f..3331cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0b18669..cb9ce8e 100644 --- a/package.json +++ b/package.json @@ -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",