From f61baaf134e307f27e9876dbe3d9cdabab6f2122 Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Wed, 18 Mar 2026 12:40:11 +0000 Subject: [PATCH] atualizacoess --- app/Aluno/AlunoHome.tsx | 274 ++++++----- app/Aluno/definicoes.tsx | 314 ++++++++----- app/Aluno/perfil.tsx | 463 +++++++++--------- app/Professor/Alunos/CalendarioPresencas.tsx | 239 +++++----- app/Professor/Alunos/DetalhesAluno.tsx | 316 ++++++------- app/Professor/Alunos/Estagios.tsx | 464 ++++++++++--------- app/Professor/Alunos/Faltas.tsx | 252 +++++----- app/Professor/Alunos/ListaAlunos.tsx | 141 +++--- app/Professor/Alunos/Presencas.tsx | 193 ++++---- app/Professor/Alunos/Sumarios.tsx | 225 +++++---- app/Professor/Empresas/DetalhesEmpresa.tsx | 182 ++++---- app/Professor/Empresas/ListaEmpresas.tsx | 148 +++--- app/Professor/PerfilProf.tsx | 223 +++------ app/Professor/ProfessorHome.tsx | 110 ++--- app/Professor/defenicoes2.tsx | 119 ++--- 15 files changed, 1920 insertions(+), 1743 deletions(-) diff --git a/app/Aluno/AlunoHome.tsx b/app/Aluno/AlunoHome.tsx index 4e9ad8d..bd38df8 100644 --- a/app/Aluno/AlunoHome.tsx +++ b/app/Aluno/AlunoHome.tsx @@ -1,6 +1,5 @@ // app/Aluno/AlunoHome.tsx import { Ionicons } from '@expo/vector-icons'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { useFocusEffect } from '@react-navigation/native'; import { decode } from 'base64-arraybuffer'; import * as DocumentPicker from 'expo-document-picker'; @@ -11,7 +10,7 @@ import { memo, useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, Animated, - Linking, + Modal, Platform, SafeAreaView, ScrollView, @@ -26,6 +25,7 @@ import { Calendar, LocaleConfig } from 'react-native-calendars'; import { useTheme } from '../../themecontext'; import { supabase } from '../lib/supabase'; +// Configuração PT-PT para o Calendário LocaleConfig.locales['pt'] = { monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'], @@ -48,34 +48,22 @@ const AlunoHome = memo(() => { const router = useRouter(); const hojeStr = new Date().toISOString().split('T')[0]; - // Estados de Dados const [selectedDate, setSelectedDate] = useState(hojeStr); - const [configEstagio, setConfigEstagio] = useState({ inicio: '2026-01-05', fim: '2026-05-30' }); + const [configEstagio, setConfigEstagio] = useState({ inicio: '', fim: '' }); const [presencas, setPresencas] = useState>({}); 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 [showLocationModal, setShowLocationModal] = useState(false); const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); const alertOpacity = useMemo(() => new Animated.Value(0), []); - const showAlert = (msg: string, type: 'success' | 'error' | 'info' = 'info') => { - setAlertConfig({ msg, type }); - Animated.sequence([ - Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }), - Animated.delay(3000), - Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) - ]).start(() => setAlertConfig(null)); - }; - const azulPetroleo = '#2390a6'; const laranjaEPVC = '#dd8707'; @@ -90,76 +78,80 @@ const AlunoHome = memo(() => { verde: '#10B981', }), [isDarkMode]); - useFocusEffect( - useCallback(() => { - carregarConfigLocal(); - fetchDadosSupabase(); - }, []) - ); - - const carregarConfigLocal = async () => { - try { - const config = await AsyncStorage.getItem('@dados_estagio'); - if (config) setConfigEstagio(JSON.parse(config)); - } catch (e) { console.error(e); } + const showAlert = (msg: string, type: 'success' | 'error' | 'info' = 'info') => { + setAlertConfig({ msg, type }); + Animated.sequence([ + Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }), + Animated.delay(3000), + Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) + ]).start(() => setAlertConfig(null)); }; + useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, [])); + const fetchDadosSupabase = async () => { setIsLoadingDB(true); try { const { data: { user } } = await supabase.auth.getUser(); if (!user) return; + const { data: estagio } = await supabase.from('estagios').select('data_inicio, data_fim').eq('aluno_id', user.id).single(); + if (estagio) setConfigEstagio({ inicio: estagio.data_inicio, fim: estagio.data_fim }); const { data, error } = await supabase.from('presencas').select('*').eq('aluno_id', user.id); if (error) throw error; - const p: any = {}, f: any = {}, s: any = {}, u: any = {}; data?.forEach(item => { if (item.estado === 'presente') { p[item.data] = true; s[item.data] = item.sumario || ''; } else { f[item.data] = true; u[item.data] = item.justificacao_url || ''; } }); setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u); - } catch (error: any) { - console.error(error); - } finally { - setIsLoadingDB(false); - } + } catch (error) { console.error(error); } + finally { setIsLoadingDB(false); } }; const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]); const infoData = useMemo(() => { const data = new Date(selectedDate); - const diaSemana = data.getDay(); + const diaSemana = data.getDay(); const nomeFeriado = feriadosMap[selectedDate]; - const fora = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim; + const antesDoInicio = configEstagio.inicio && selectedDate < configEstagio.inicio; + const depoisDoFim = configEstagio.fim && selectedDate > configEstagio.fim; return { - valida: diaSemana !== 0 && diaSemana !== 6 && !fora && !nomeFeriado, - podeMarcar: selectedDate === hojeStr && !fora && !nomeFeriado, - nomeFeriado + valida: diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado, + podeMarcar: selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado, + nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: antesDoInicio || depoisDoFim }; }, [selectedDate, configEstagio, hojeStr, feriadosMap]); - const handlePresenca = async () => { - if (!infoData.podeMarcar) return showAlert("Não podes marcar presença nesta data.", "error"); + const handlePresencaClick = async () => { + const { status } = await Location.getForegroundPermissionsAsync(); + if (status === 'granted') { + executarMarcacao(); + } else { + setShowLocationModal(true); + } + }; + + const executarMarcacao = async () => { + setShowLocationModal(false); setIsLocating(true); try { const { status } = await Location.requestForegroundPermissionsAsync(); - if (status !== 'granted') throw new Error("Sem permissão de GPS."); - const loc = await Location.getCurrentPositionAsync({}); + if (status !== 'granted') throw new Error("Sem acesso ao GPS."); + const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); const { data: { user } } = await supabase.auth.getUser(); - const { error } = await supabase.from('presencas').upsert({ + await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude }); - if (error) throw error; - showAlert("Presença registada com sucesso!", "success"); + showAlert("Presença marcada com sucesso!", "success"); fetchDadosSupabase(); - } catch (e: any) { - showAlert(e.message, "error"); - } finally { setIsLocating(false); } + } catch (e: any) { showAlert(e.message, "error"); } + finally { setIsLocating(false); } }; const handleFalta = async () => { - if (!infoData.valida) return showAlert("Data inválida para registar falta.", "error"); + if (infoData.foraDeRange) return showAlert("Fora do período de estágio.", "error"); + if (!infoData.valida) return showAlert("Não podes marcar falta neste dia.", "error"); try { const { data: { user } } = await supabase.auth.getUser(); await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' }); @@ -168,6 +160,29 @@ const AlunoHome = memo(() => { } catch (e) { showAlert("Erro ao registar falta.", "error"); } }; + const selecionarDocumento = async () => { + const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' }); + if (!res.canceled) setPdf(res.assets[0]); + }; + + const enviarJustificativo = async () => { + if (!pdf) return; + setIsUploading(true); + try { + const { data: { user } } = await supabase.auth.getUser(); + const fileBase64 = await FileSystem.readAsStringAsync(pdf.uri, { encoding: FileSystem.EncodingType.Base64 }); + const fileName = `${user?.id}/${selectedDate}_justificacao.pdf`; + const { error: uploadError } = await supabase.storage.from('justificacoes').upload(fileName, decode(fileBase64), { contentType: 'application/pdf', upsert: true }); + if (uploadError) throw uploadError; + const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName); + await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate }); + setPdf(null); + showAlert("Justificativo enviado!", "success"); + fetchDadosSupabase(); + } catch (e) { showAlert("Erro no upload.", "error"); } + finally { setIsUploading(false); } + }; + const guardarSumario = async () => { try { const { data: { user } } = await supabase.auth.getUser(); @@ -178,59 +193,54 @@ const AlunoHome = memo(() => { } catch (e) { showAlert("Erro ao guardar sumário.", "error"); } }; - const enviarJustificacao = async () => { - if (!pdf) return; - setIsUploading(true); - try { - const { data: { user } } = await supabase.auth.getUser(); - const fileName = `${user?.id}/${selectedDate}_${Date.now()}.pdf`; - const base64 = await FileSystem.readAsStringAsync(pdf.uri, { encoding: 'base64' }); - const { error: upErr } = await supabase.storage.from('justificacoes').upload(fileName, decode(base64), { contentType: 'application/pdf' }); - if (upErr) throw upErr; - - const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName); - await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate }); - - showAlert("Documento enviado!", "success"); - setPdf(null); - fetchDadosSupabase(); - } catch (e: any) { showAlert(e.message, "error"); } - finally { setIsUploading(false); } - }; - - const visualizarDocumento = async (url: string) => { - const supported = await Linking.canOpenURL(url); - if (supported) await Linking.openURL(url); - else showAlert("Não foi possível abrir o ficheiro.", "error"); - }; - return ( - - {/* ALERT OVERLAY MODERNO */} + + + + + + + + + Validar Localização + + Para registar a tua presença, precisamos de confirmar que te encontras no local de estágio. + + + Confirmar e Marcar + + setShowLocationModal(false)}> + Agora não + + + + + {alertConfig && ( - - + {alertConfig.msg} )} - router.push('/Aluno/perfil')}> - - Estágios+ - router.push('/Aluno/definicoes')}> - - + + router.push('/Aluno/definicoes')} style={{ marginRight: 15 }}> + + + router.push('/Aluno/perfil')}> + + + {isLocating ? : Marcar Presença} @@ -240,21 +250,22 @@ const AlunoHome = memo(() => { onPress={handleFalta} disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]} > - Faltei + Marcar Falta - {isLoadingDB && } ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}), @@ -262,46 +273,48 @@ const AlunoHome = memo(() => { ...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#EF4444' } }), {}), [selectedDate]: { selected: true, selectedColor: azulPetroleo } }} - onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }} + onDayPress={(day) => setSelectedDate(day.dateString)} /> {presencas[selectedDate] && ( - Sumário - {!editandoSumario && setEditandoSumario(true)}>} + Sumário do Dia + setEditandoSumario(true)}> setSumarios({...sumarios, [selectedDate]: txt})} - placeholder="O que fizeste hoje?" + placeholder="Descreve o que fizeste..." placeholderTextColor="#94A3B8" /> - {editandoSumario && Guardar} + {editandoSumario && Guardar Sumário} )} {faltas[selectedDate] && ( - Justificação + Justificar Falta {urlsJustificacao[selectedDate] ? ( - visualizarDocumento(urlsJustificacao[selectedDate])}> - - Ver PDF - - ) : ( - - { - const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' }); - if (!res.canceled) setPdf(res.assets[0]); - }}> - {pdf ? pdf.name : 'Selecionar PDF'} - - {pdf && {isUploading ? : Enviar}} + + + Documento Enviado + ) : ( + <> + + + {pdf ? pdf.name : "Selecionar PDF"} + + {pdf && ( + + {isUploading ? : Submeter Justificativo} + + )} + )} )} @@ -312,24 +325,33 @@ const AlunoHome = memo(() => { const styles = StyleSheet.create({ safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, - alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 999, elevation: 10 }, - alertText: { color: '#fff', fontWeight: 'bold', marginLeft: 10, flex: 1 }, - container: { padding: 20, paddingBottom: 40 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - title: { fontSize: 22, fontWeight: '800' }, + container: { padding: 20 }, + topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 25 }, + topIcons: { flexDirection: 'row', alignItems: 'center' }, + title: { fontSize: 26, fontWeight: '900' }, + alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 15, zIndex: 1000 }, + alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' }, botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 }, - btn: { padding: 16, borderRadius: 16, width: '48%', alignItems: 'center' }, - btnAcao: { padding: 14, borderRadius: 12, marginTop: 10, alignItems: 'center' }, - btnAnexar: { borderWidth: 1, padding: 14, borderRadius: 12, marginBottom: 10, alignItems: 'center', borderStyle: 'dashed' }, - btnVer: { padding: 14, borderRadius: 12, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }, + btn: { padding: 18, borderRadius: 22, width: '48%', alignItems: 'center', elevation: 3 }, + txtBtn: { color: '#fff', fontWeight: '800', fontSize: 14 }, disabled: { opacity: 0.4 }, - txtBtn: { color: '#fff', fontWeight: 'bold' }, - cardCalendar: { borderRadius: 24, padding: 10, borderWidth: 1, position: 'relative', overflow: 'hidden' }, - loaderOverlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.4)', zIndex: 10 }, - card: { padding: 20, borderRadius: 24, marginTop: 20, borderWidth: 1 }, - cardTitulo: { fontSize: 17, fontWeight: '700' }, - rowTitle: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }, - input: { borderWidth: 1, borderRadius: 12, padding: 15, height: 100, textAlignVertical: 'top' } + cardCalendar: { borderRadius: 30, padding: 10, borderWidth: 1, overflow: 'hidden' }, + card: { padding: 20, borderRadius: 25, marginTop: 20, borderWidth: 1 }, + cardTitulo: { fontSize: 18, fontWeight: '700' }, + rowTitle: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 }, + input: { borderWidth: 1, borderRadius: 15, padding: 15, height: 100, textAlignVertical: 'top' }, + btnSalvar: { padding: 15, borderRadius: 15, marginTop: 15, alignItems: 'center' }, + btnUpload: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 15, borderRadius: 15, borderWidth: 2, borderStyle: 'dashed' }, + justificadoBox: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 10 }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, + locationModal: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 30, alignItems: 'center', paddingBottom: 50 }, + modalHandle: { width: 50, height: 6, backgroundColor: '#cbd5e1', borderRadius: 10, marginBottom: 30 }, + iconCircle: { width: 90, height: 90, borderRadius: 45, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + modalTitle: { fontSize: 24, fontWeight: '800', marginBottom: 12 }, + modalDesc: { fontSize: 16, textAlign: 'center', lineHeight: 24, marginBottom: 35, paddingHorizontal: 15 }, + btnConfirmar: { width: '100%', padding: 18, borderRadius: 20, alignItems: 'center' }, + btnFechar: { marginTop: 20 }, + txtFechar: { fontWeight: '700', fontSize: 16 } }); export default AlunoHome; \ No newline at end of file diff --git a/app/Aluno/definicoes.tsx b/app/Aluno/definicoes.tsx index 444709e..be17851 100644 --- a/app/Aluno/definicoes.tsx +++ b/app/Aluno/definicoes.tsx @@ -1,11 +1,11 @@ +// app/Definicoes.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useMemo, useState } from 'react'; // Importado useMemo e memo +import { memo, useCallback, useMemo, useState } from 'react'; import { - Alert, + Animated, Linking, - Platform, - SafeAreaView, + ScrollView, StatusBar, StyleSheet, Switch, @@ -13,149 +13,219 @@ import { TouchableOpacity, View } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../themecontext'; +import { supabase } from '../lib/supabase'; const Definicoes = memo(() => { const router = useRouter(); - const [notificacoes, setNotificacoes] = useState(true); + const insets = useSafeAreaInsets(); const { isDarkMode, toggleTheme } = useTheme(); + const [notificacoes, setNotificacoes] = useState(true); + + // --- SISTEMA DE AVISOS MODERNOS --- + const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); + const alertOpacity = useMemo(() => new Animated.Value(0), []); + + const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => { + setAlertConfig({ msg, type }); + Animated.sequence([ + Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }), + Animated.delay(2500), + Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) + ]).start(() => setAlertConfig(null)); + }, []); + + const azulPetroleo = '#2390a6'; - // Otimização de cores para evitar lag no render const cores = useMemo(() => ({ - fundo: isDarkMode ? '#121212' : '#f1f3f5', - card: isDarkMode ? '#1e1e1e' : '#fff', - texto: isDarkMode ? '#ffffff' : '#000000', - textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - borda: isDarkMode ? '#333' : '#f1f3f5', - sair: '#dc3545', - azul: '#0d6efd' + fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', + card: isDarkMode ? '#1A1A1A' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', + azul: azulPetroleo, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + vermelho: '#EF4444', + vermelhoSuave: 'rgba(239, 68, 68, 0.1)', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + verde: '#10B981', }), [isDarkMode]); - const handleLogout = () => { - Alert.alert( - "Terminar Sessão", - "Tem a certeza que deseja sair da aplicação?", - [ - { text: "Cancelar", style: "cancel" }, - { - text: "Sair", - style: "destructive", - onPress: () => router.replace('/') - } - ] - ); + const handleLogout = async () => { + try { + await supabase.auth.signOut(); + router.replace('/'); + } catch (e) { + showAlert("Erro ao sair da conta", "error"); + } }; - const abrirEmail = () => Linking.openURL(`mailto:epvc@epvc.pt`); - const abrirEmail2 = () => Linking.openURL(`mailto:secretaria@epvc.pt`); - const abrirTelefone = () => Linking.openURL('tel:252 641 805'); + const abrirURL = (url: string) => { + Linking.canOpenURL(url).then(supported => { + if (supported) Linking.openURL(url); + else showAlert("Não foi possível abrir o link", "error"); + }); + }; return ( - + - - - router.back()} - > - - - Definições - - - - - - Preferências - - - - - Notificações - - - - - - - - Modo escuro - - - - - Suporte e Contactos - - - - - Direção - - epvc@epvc.pt - - - - - - Secretaria - - secretaria@epvc.pt - - - - - - Ligar para a Escola - - 252 641 805 - - - - - - Versão da app - - 26.1.10 - + {/* BANNER DE FEEDBACK */} + {alertConfig && ( + + + {alertConfig.msg} + + )} + + + {/* HEADER ESTILIZADO */} + router.back()} > - - - Terminar Sessão - - + - + Definições + - - + + + + {/* GRUPO: PERSONALIZAÇÃO */} + Personalização + + + + + + Notificações Push + { + setNotificacoes(v); + showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info"); + }} + trackColor={{ false: '#CBD5E1', true: cores.azul }} + thumbColor="#FFFFFF" + /> + + + + + + + Interface Escura + + + + + {/* 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} + /> + + + {/* GRUPO: SEGURANÇA & INFO */} + + + + + + Versão Estável + v2.6.17 + + + + + + + Terminar Sessão + + + + + + Desenvolvido para PAP • 2026 + EPVC + + + + + ); }); +// Componente Auxiliar para Links +const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => ( + + + + + + {label} + {subLabel} + + + +); + const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, + safe: { flex: 1 }, + alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 8 }, + alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1, fontSize: 14 }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 }, - btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 }, - tituloGeral: { fontSize: 24, fontWeight: 'bold' }, - subtituloSecao: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase', marginBottom: 5, marginLeft: 5 }, - container: { padding: 20 }, - card: { paddingHorizontal: 20, paddingVertical: 15, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 }, - linha: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 15, borderBottomWidth: 1 }, - iconTexto: { flexDirection: 'row', alignItems: 'center' } + btnVoltar: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 50 }, + sectionLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 1.2 }, + card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 15, elevation: 2 }, + item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16 }, + iconContainer: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700' }, + versionBadge: { fontSize: 13, fontWeight: 'bold', backgroundColor: 'rgba(0,0,0,0.05)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + footer: { alignItems: 'center', marginTop: 40, opacity: 0.6 }, + footerText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 } }); export default Definicoes; \ No newline at end of file diff --git a/app/Aluno/perfil.tsx b/app/Aluno/perfil.tsx index 335f548..2ad34b3 100644 --- a/app/Aluno/perfil.tsx +++ b/app/Aluno/perfil.tsx @@ -1,25 +1,55 @@ +// app/Aluno/PerfilAluno.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { - ActivityIndicator, Alert, Platform, SafeAreaView, ScrollView, StatusBar, + ActivityIndicator, Animated, + ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../themecontext'; import { supabase } from '../lib/supabase'; export default function PerfilAluno() { const { isDarkMode } = useTheme(); const router = useRouter(); + const insets = useSafeAreaInsets(); + const [loading, setLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); - const [perfil, setPerfil] = useState(null); const [estagio, setEstagio] = useState(null); const [contagemPresencas, setContagemPresencas] = useState(0); const [contagemFaltas, setContagemFaltas] = useState(0); - // --- FUNÇÕES DE FORMATAÇÃO E CÁLCULO --- + const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); + const alertOpacity = useMemo(() => new Animated.Value(0), []); + + const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => { + setAlertConfig({ msg, type }); + Animated.sequence([ + Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }), + Animated.delay(3000), + Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true }) + ]).start(() => setAlertConfig(null)); + }, []); + + const azulPetroleo = '#2390a6'; + + const cores = useMemo(() => ({ + fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', + card: isDarkMode ? '#1A1A1A' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', + azul: azulPetroleo, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + vermelho: '#EF4444', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + verde: '#10B981', + laranja: '#dd8707', + }), [isDarkMode]); const formatarParaExibir = (data: string) => { if (!data) return ''; @@ -41,64 +71,33 @@ export default function PerfilAluno() { return formatted; }; - // NOVA FUNÇÃO: Calcula horas totais excluindo fins de semana e feriados const calcularHorasTotaisUteis = (dataInicio: string, dataFim: string) => { - if (!dataInicio || !dataFim) return 400; // Fallback caso a BD esteja vazia - + if (!dataInicio || !dataFim) return 400; let inicio = new Date(dataInicio); let fim = new Date(dataFim); let diasUteis = 0; - - // Lista de feriados 2026 (Exemplo Portugal) - const feriados = [ - '2026-01-01', '2026-04-03', '2026-04-05', '2026-04-25', - '2026-05-01', '2026-06-04', '2026-06-10', '2026-08-15', - '2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25' - ]; + const feriados = ['2026-01-01', '2026-04-03', '2026-04-25', '2026-05-01', '2026-06-10', '2026-08-15', '2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25']; let dataAtual = new Date(inicio); while (dataAtual <= fim) { - const diaSemana = dataAtual.getDay(); // 0: Domingo, 6: Sábado + const diaSemana = dataAtual.getDay(); const dataIso = dataAtual.toISOString().split('T')[0]; - - if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) { - diasUteis++; - } + if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) diasUteis++; dataAtual.setDate(dataAtual.getDate() + 1); } - return diasUteis * 7; // Multiplicado pelas 7h diárias + return diasUteis * 7; }; - const calcularIdade = (dataExibicao: string) => { - if (!dataExibicao || dataExibicao.length < 10) return '--'; - try { - const [dia, mes, ano] = dataExibicao.split('-').map(Number); - const hoje = new Date(); - const nascimento = new Date(ano, mes - 1, dia); - let idade = hoje.getFullYear() - nascimento.getFullYear(); - const m = hoje.getMonth() - nascimento.getMonth(); - if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) { - idade--; - } - return isNaN(idade) ? '--' : idade; - } catch { - return '--'; - } - }; - - useEffect(() => { - carregarDados(); - }, []); - - async function carregarDados() { + const carregarDados = async () => { try { + setLoading(true); const { data: { user } } = await supabase.auth.getUser(); if (!user) return; const [perfilRes, estagioRes, presencasRes] = await Promise.all([ supabase.from('profiles').select('*').eq('id', user.id).single(), supabase.from('estagios').select('*, empresas(*)').eq('aluno_id', user.id).single(), - supabase.from('presencas').select('estado').eq('aluno_id', user.id) + supabase.from('presencas').select('*').eq('aluno_id', user.id) ]); const dadosPerfil = perfilRes.data; @@ -107,23 +106,34 @@ export default function PerfilAluno() { } setPerfil({ ...dadosPerfil, email: user.email }); - setEstagio(estagioRes.data); + const dadosEstagio = estagioRes.data; + setEstagio(dadosEstagio); - if (presencasRes.data) { - setContagemPresencas(presencasRes.data.filter((p: any) => p.estado === 'presente').length); - setContagemFaltas(presencasRes.data.filter((p: any) => p.estado === 'faltou').length); + if (presencasRes.data && dadosEstagio) { + const dataInicio = new Date(dadosEstagio.data_inicio); + const dataFim = new Date(dadosEstagio.data_fim); + + // Filtrar apenas presenças que ocorrem DENTRO do intervalo do estágio + const presencasValidas = presencasRes.data.filter((p: any) => { + const dataP = new Date(p.data); + return dataP >= dataInicio && dataP <= dataFim; + }); + + setContagemPresencas(presencasValidas.filter((p: any) => p.estado === 'presente').length); + setContagemFaltas(presencasValidas.filter((p: any) => p.estado === 'faltou').length); } } catch (e) { - console.error(e); + showAlert('Erro ao carregar dados.', 'error'); } finally { setLoading(false); } - } + }; + + useEffect(() => { carregarDados(); }, []); const salvarPerfil = async () => { try { const dataBD = formatarParaSalvar(perfil.data_nascimento); - const { error } = await supabase.from('profiles').update({ nome: perfil.nome, telefone: perfil.telefone, @@ -133,220 +143,207 @@ export default function PerfilAluno() { if (error) throw error; setIsEditing(false); - Alert.alert("Sucesso", "Perfil atualizado!"); + showAlert('Perfil atualizado!', 'success'); } catch (e) { - Alert.alert("Erro", "Verifica se a data está correta (DD-MM-AAAA)."); + showAlert('Verifica se a data está correta.', 'error'); } }; - // CÁLCULOS DO DASHBOARD - const horasPorDia = 7; - const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim); - const horasRealizadas = contagemPresencas * horasPorDia; - const horasRestantes = Math.max(0, horasTotais - horasRealizadas); - const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0; - - const themeStyles = { - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: '#3B82F6', - borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - vermelho: '#EF4444' + const terminarSessao = async () => { + await supabase.auth.signOut(); + router.replace('/'); }; - if (loading) return ; + const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim); + const horasRealizadas = contagemPresencas * 7; + const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0; + + if (loading) return ; return ( - - + + - - router.back()} style={styles.backBtn}> - - - + {alertConfig && ( + + + {alertConfig.msg} + + )} - - - - - {perfil?.nome?.charAt(0).toUpperCase()} - - {perfil?.nome} - + + + router.back()}> + + + O Meu Perfil setIsEditing(true)} + style={[styles.editBtn, { backgroundColor: isEditing ? cores.azul : cores.card }]} + onPress={() => isEditing ? salvarPerfil() : setIsEditing(true)} > - - - {isEditing ? "Guardar" : "Editar Perfil"} - + - {/* PROGRESSO */} - - Progresso do Estágio - - - - - - - - - + + + + + + {perfil?.nome?.charAt(0).toUpperCase()} - - {Math.round(progresso * 100)}% das {horasTotais}h concluídas (Dias Úteis) - + + {perfil?.nome} + Nº Aluno: {perfil?.n_escola || '---'} + + + {/* PROGRESS CARD */} + + Progresso do Estágio + + + {horasRealizadas}h + Feitas + + + {Math.max(0, horasTotais - horasRealizadas)}h + Restam + + + {contagemFaltas} + Faltas + + + + + + {Math.round(progresso * 100)}% das {horasTotais}h úteis concluídas + + + {/* INFO CARD */} + + setPerfil({...perfil, nome: v})} cores={cores} /> + + + + {/* Aumentado o flex da data para 1.2 para evitar corte */} + + setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})} + cores={cores} + maxLength={10} + keyboardType="numeric" + placeholder="DD-MM-AAAA" + /> + + + setPerfil({...perfil, telefone: v})} keyboardType="phone-pad" cores={cores} /> + + + + setPerfil({...perfil, residencia: v})} cores={cores} /> + + + {/* ESTÁGIO CARD ATUALIZADO */} + + Informações do Estágio + + + Início + {formatarParaExibir(estagio?.data_inicio)} + + + Fim + {formatarParaExibir(estagio?.data_fim)} + + + Horário + {estagio?.horario || '09:00-17:00'} + + + + Empresa e Tutor + {estagio?.empresas?.nome} + {estagio?.empresas?.tutor_nome} - - {/* DADOS ACADÉMICOS */} - - Dados de Aluno - - - - + + router.push('/Aluno/redefenirsenha')}> + + + + Alterar Palavra-passe + + + + + + + + Terminar Sessão + - - {/* INFORMAÇÃO PESSOAL */} - - Informação Pessoal - - setPerfil({...perfil, nome: t})} theme={themeStyles} /> - - - setPerfil({...perfil, data_nascimento: aplicarMascaraData(t)})} - theme={themeStyles} - keyboard="numeric" - limit={10} - /> - - + {isEditing && ( + { setIsEditing(false); carregarDados(); }}> + Cancelar Edição + + )} - - - - setPerfil({...perfil, telefone: t})} theme={themeStyles} keyboard="phone-pad" /> - - setPerfil({...perfil, residencia: t})} theme={themeStyles} /> - - - - {/* EMPRESA E DATAS DO ESTÁGIO */} - - Horário e Local - - - - - - - - - - - - - - - - ); -} - -// --- AUXILIARES (IGUAIS) --- -function StatBox({ label, valor, cor, theme }: any) { - return ( - - {valor} - {label} + + ); } -function EditableRow({ icon, label, value, isEditing, onChange, theme, keyboard = "default", limit }: any) { - return ( - - - - {label} - {isEditing ? ( - - ) : ( - {value || '---'} - )} - +const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( + + {label} + + + - ); -} - -function InfoRow({ icon, label, valor, theme }: any) { - return ( - - - - {label} - {valor || '---'} - - - ); -} - -const Divider = ({ theme }: any) => ; + +); const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, - topNav: { paddingHorizontal: 20, paddingTop: 10, height: 50, justifyContent: 'center' }, - backBtn: { width: 40, height: 40, justifyContent: 'center' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, + safe: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - header: { alignItems: 'center', marginBottom: 30 }, - avatarCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 12 }, - avatarText: { fontSize: 32, fontWeight: 'bold', color: '#fff' }, - userName: { fontSize: 20, fontWeight: '800', marginBottom: 15 }, - btnEdit: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, borderWidth: 1 }, - btnEditText: { marginLeft: 8, fontSize: 13, fontWeight: '600' }, - section: { marginBottom: 25 }, - sectionTitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10, marginLeft: 5 }, - card: { borderRadius: 20, padding: 16, borderWidth: 1 }, - statsGrid: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 20 }, + alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 15, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 10 }, + alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 }, + topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 }, + topTitle: { fontSize: 18, fontWeight: '800' }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, + profileHeader: { alignItems: 'center', marginVertical: 20 }, + avatarContainer: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' }, + avatar: { width: 70, height: 70, borderRadius: 35, alignItems: 'center', justifyContent: 'center', elevation: 4 }, + avatarLetter: { color: '#fff', fontSize: 28, fontWeight: '800' }, + userName: { fontSize: 20, fontWeight: '800', marginTop: 12 }, + userRole: { fontSize: 13, fontWeight: '500' }, + card: { borderRadius: 24, padding: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, + statsGrid: { flexDirection: 'row', justifyContent: 'space-around' }, statBox: { alignItems: 'center' }, - statValor: { fontSize: 18, fontWeight: 'bold' }, - statLabel: { fontSize: 11, fontWeight: '600', marginTop: 2 }, - progressContainer: { marginTop: 10 }, + statValor: { fontSize: 18, fontWeight: '800' }, + statLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase' }, progressBarBase: { height: 8, borderRadius: 4, overflow: 'hidden' }, progressBarFill: { height: '100%', borderRadius: 4 }, - progressText: { fontSize: 11, textAlign: 'center', marginTop: 8, fontWeight: '600' }, - infoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8 }, - infoContent: { flex: 1 }, - infoLabel: { fontSize: 10, fontWeight: '600', textTransform: 'uppercase' }, - infoValor: { fontSize: 15, fontWeight: '600', marginTop: 2 }, - input: { fontSize: 15, fontWeight: '600', paddingVertical: 2, borderBottomWidth: 1 }, - divider: { height: 1, width: '100%', marginVertical: 8 } + progressText: { fontSize: 10, textAlign: 'center', marginTop: 8, fontWeight: '700' }, + inputWrapper: { marginBottom: 15 }, + inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, height: 50, borderRadius: 16, borderWidth: 1.5 }, + textInput: { flex: 1, fontSize: 14, fontWeight: '600' }, + row: { flexDirection: 'row' }, + actionsContainer: { gap: 10 }, + menuItem: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 18, elevation: 1 }, + menuIcon: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + menuText: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + cancelBtn: { marginTop: 20, alignItems: 'center' }, + cancelText: { fontSize: 13, fontWeight: '600', textDecorationLine: 'underline' } }); \ No newline at end of file diff --git a/app/Professor/Alunos/CalendarioPresencas.tsx b/app/Professor/Alunos/CalendarioPresencas.tsx index 770379e..274b8f2 100644 --- a/app/Professor/Alunos/CalendarioPresencas.tsx +++ b/app/Professor/Alunos/CalendarioPresencas.tsx @@ -1,3 +1,4 @@ +// app/(Professor)/HistoricoPresencas.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useEffect, useMemo, useState } from 'react'; @@ -40,15 +41,16 @@ export default function HistoricoPresencas() { const nomeStr = Array.isArray(params.nome) ? params.nome[0] : params.nome; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: '#3B82F6', - azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)', - borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#10B981', - vermelho: '#EF4444', + fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#FFFFFF' : '#1A1C1E', + secundario: isDarkMode ? '#8E8E93' : '#6C757D', + azul: '#2390a6', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + vermelho: '#FF453A', + verde: '#32D74B', + borda: isDarkMode ? '#2C2C2E' : '#E9ECEF', + linha: isDarkMode ? '#2C2C2E' : '#D1D9E6', }), [isDarkMode]); useEffect(() => { @@ -58,16 +60,41 @@ export default function HistoricoPresencas() { async function fetchHistorico() { try { setLoading(true); + const hoje = new Date().toISOString().split('T')[0]; + + // 1. Procurar o estágio ativo (mesma lógica das Faltas) + const { data: listaEstagios, error: errEstagio } = await supabase + .from('estagios') + .select('data_inicio, data_fim') + .lte('data_inicio', hoje) + .gte('data_fim', hoje) + .limit(1); + + if (errEstagio) throw errEstagio; + + // Fallback caso não haja estágio hoje (Janeiro a Dezembro de 2026) + let inicio = '2026-01-01'; + let fim = '2026-12-31'; + + if (listaEstagios && listaEstagios.length > 0) { + inicio = listaEstagios[0].data_inicio; + fim = listaEstagios[0].data_fim; + } + + // 2. Buscar histórico filtrado por esse período const { data, error } = await supabase .from('presencas') .select('*') .eq('aluno_id', idStr) + .gte('data', inicio) + .lte('data', fim) .order('data', { ascending: false }); if (error) throw error; setPresencas(data || []); } catch (error: any) { - Alert.alert("Erro", "Não foi possível carregar as presenças."); + console.error(error); + Alert.alert("Erro", "Não foi possível carregar as presenças do estágio atual."); } finally { setLoading(false); } @@ -97,9 +124,7 @@ export default function HistoricoPresencas() { }); if (url) { - Linking.openURL(url).catch(() => { - Alert.alert("Erro", "Não foi possível abrir o mapa."); - }); + Linking.openURL(url).catch(() => Alert.alert("Erro", "Não foi possível abrir o mapa.")); } }; @@ -107,26 +132,25 @@ export default function HistoricoPresencas() { - {/* HEADER PREMIUM */} - - + + router.back()} - style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} > - + - - Histórico - {nomeStr} + + Histórico + {nomeStr} - + @@ -138,63 +162,65 @@ export default function HistoricoPresencas() { ) : ( {presencas.length === 0 ? ( - - - Sem registos para este aluno. + + + + + Sem registos neste estágio + Não foram encontradas presenças ou faltas para o período selecionado. ) : ( - presencas.map((item) => { - const isPresente = item.estado === 'presente'; - - return ( - handleNavigation(item)} - style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]} - > - - - - - - - - {new Date(item.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })} - - - - - {item.estado ? item.estado.toUpperCase() : '---'} - + + + + {presencas.map((item) => { + const isPresente = item.estado === 'presente'; + const dataObj = new Date(item.data); + + return ( + + + + handleNavigation(item)} + style={[styles.modernCard, { backgroundColor: cores.card }]} + > + + + + {dataObj.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })} + + + {dataObj.getFullYear()} + + + + + + + + + {isPresente ? 'PRESENÇA MARCADA' : 'FALTA REGISTADA'} + + {item.lat && item.lng && ( + abrirMapa(item.lat!, item.lng!)} style={styles.mapSmallBtn}> + + + )} + + + Clique para ver {isPresente ? 'o sumário' : 'a justificação'} + + + + - - - - {item.lat && item.lng && ( - abrirMapa(item.lat!, item.lng!)} - style={styles.locationBtn} - > - - - )} - - + - - - - Ver {isPresente ? 'sumário detalhado' : 'comprovativo de falta'} - - - - ); - }) + ); + })} + )} )} @@ -204,40 +230,31 @@ export default function HistoricoPresencas() { const styles = StyleSheet.create({ safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 }, - headerFixed: { paddingHorizontal: 20, paddingBottom: 10 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 75 }, - backBtnPremium: { - width: 42, height: 42, borderRadius: 14, - justifyContent: 'center', alignItems: 'center', - borderWidth: 1, elevation: 2, shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2 - }, - refreshBtnPremium: { - width: 42, height: 42, borderRadius: 14, - justifyContent: 'center', alignItems: 'center', - borderWidth: 1 - }, - titleContainer: { flex: 1, alignItems: 'center', paddingHorizontal: 10 }, - title: { fontSize: 20, fontWeight: '800' }, - subtitle: { fontSize: 12, fontWeight: '600', marginTop: -2 }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 30 }, - card: { - borderRadius: 22, padding: 16, marginBottom: 15, - borderWidth: 1, elevation: 3, shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 - }, - cardHeader: { flexDirection: 'row', alignItems: 'center' }, - iconBox: { width: 44, height: 44, borderRadius: 15, justifyContent: 'center', alignItems: 'center' }, - info: { flex: 1, marginLeft: 15 }, - dateText: { fontSize: 15, fontWeight: '700' }, - statusBadge: { flexDirection: 'row', alignItems: 'center', marginTop: 4 }, - dot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, - subText: { fontSize: 10, fontWeight: '800', letterSpacing: 0.5 }, - actionRow: { flexDirection: 'row', alignItems: 'center', gap: 10 }, - locationBtn: { width: 34, height: 34, justifyContent: 'center', alignItems: 'center' }, - footerCard: { marginTop: 12, paddingTop: 10, borderTopWidth: 1 }, - tapInfo: { fontSize: 10, fontWeight: '800', textAlign: 'right', textTransform: 'uppercase', letterSpacing: 0.5 }, + headerArea: { paddingHorizontal: 24, paddingBottom: 15, paddingTop: 10 }, + topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 60 }, + btnAction: { width: 45, height: 45, borderRadius: 16, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + titleWrapper: { alignItems: 'center', flex: 1 }, + headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 }, + scrollContent: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 50 }, + timelineWrapper: { paddingLeft: 10 }, + timelineLine: { position: 'absolute', left: 10, top: 0, bottom: 0, width: 2, borderRadius: 1 }, + timelineItem: { marginBottom: 20, paddingLeft: 25, justifyContent: 'center' }, + timelineDot: { position: 'absolute', left: 6, top: '50%', marginTop: -5, width: 10, height: 10, borderRadius: 5, borderWidth: 2, zIndex: 1 }, + modernCard: { borderRadius: 20, padding: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 }, + cardMain: { flexDirection: 'row', alignItems: 'center' }, + dateInfo: { alignItems: 'center', minWidth: 50 }, + dayText: { fontSize: 15, fontWeight: '800', textTransform: 'uppercase' }, + yearText: { fontSize: 11, fontWeight: '600' }, + dividerVertical: { width: 1, height: 30, backgroundColor: 'rgba(128,128,128,0.2)', marginHorizontal: 15 }, + contentInfo: { flex: 1 }, + statusRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 2 }, + statusText: { fontSize: 11, fontWeight: '900', letterSpacing: 0.5 }, + mapSmallBtn: { marginLeft: 8 }, + infoHint: { fontSize: 12, fontWeight: '500' }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - empty: { alignItems: 'center', marginTop: 60 }, - emptyText: { marginTop: 15, fontSize: 14, fontWeight: '600', textAlign: 'center' }, + emptyContainer: { alignItems: 'center', marginTop: 100 }, + emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + emptyText: { fontSize: 18, fontWeight: '800', marginBottom: 8 }, + emptySub: { fontSize: 14, textAlign: 'center', paddingHorizontal: 40, lineHeight: 20 }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/DetalhesAluno.tsx b/app/Professor/Alunos/DetalhesAluno.tsx index 2643e53..5a17bb1 100644 --- a/app/Professor/Alunos/DetalhesAluno.tsx +++ b/app/Professor/Alunos/DetalhesAluno.tsx @@ -4,55 +4,41 @@ import { useLocalSearchParams, useRouter } from 'expo-router'; import { memo, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, + Linking, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, - View, + View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; -interface AlunoEstado { - id: string; - nome: string; - n_escola: string; - turma_curso: string; - email: string; - telefone: string; - residencia: string; - idade: string; - empresa_nome: string; - tutor_nome: string; - tutor_tel: string; - data_inicio: string; - data_fim: string; - horas_diarias: string; - horarios_detalhados: string[]; -} - const DetalhesAlunos = memo(() => { const router = useRouter(); const params = useLocalSearchParams(); const { isDarkMode } = useTheme(); const insets = useSafeAreaInsets(); - const azulPetroleo = '#2390a6'; + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + verde: '#10B981', }), [isDarkMode]); const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null; - const [aluno, setAluno] = useState(null); + const [aluno, setAluno] = useState(null); const [loading, setLoading] = useState(true); const fetchAluno = async () => { @@ -72,190 +58,196 @@ const DetalhesAlunos = memo(() => { .single(); if (error) throw error; - if (data) { - const d = data as any; + + let listaHorarios: string[] = []; + if (data?.estagios?.[0]?.id) { + const { data: hData } = await supabase + .from('horarios_estagio') + .select('hora_inicio, hora_fim') + .eq('estagio_id', data.estagios[0].id); - // Garante que pegamos o objeto correto mesmo que venha em Array - const perfil = Array.isArray(d.profiles) ? d.profiles[0] : d.profiles; - const estagio = Array.isArray(d.estagios) ? d.estagios[0] : d.estagios; - const empresa = estagio && (Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas); - - let listaHorarios: string[] = []; - if (estagio?.id) { - const { data: hData, error: hError } = await supabase - .from('horarios_estagio') - .select('hora_inicio, hora_fim') - .eq('estagio_id', estagio.id); - - if (!hError && hData) { - listaHorarios = hData.map(h => { - // Limpa os segundos caso venham da base de dados (ex: 08:00:00 -> 08:00) - const inicio = h.hora_inicio?.slice(0, 5) || '00:00'; - const fim = h.hora_fim?.slice(0, 5) || '00:00'; - return `${inicio} às ${fim}`; - }); - } + if (hData) { + listaHorarios = hData.map(h => `${h.hora_inicio?.slice(0, 5)} - ${h.hora_fim?.slice(0, 5)}`); } - - setAluno({ - id: String(d.id), - nome: d.nome || 'Sem nome', - n_escola: String(d.n_escola || '-'), - turma_curso: d.turma_curso || '-', - email: perfil?.email ?? '-', - telefone: perfil?.telefone ?? '-', - residencia: perfil?.residencia ?? '-', - idade: perfil?.idade ? String(perfil.idade) : '-', - empresa_nome: empresa?.nome || 'Não atribuída', - tutor_nome: empresa?.tutor_nome || 'Não definido', - tutor_tel: empresa?.tutor_telefone || '-', - data_inicio: estagio?.data_inicio || '-', - data_fim: estagio?.data_fim || '-', - horas_diarias: estagio?.horas_diarias ? `${estagio.horas_diarias}h` : '0h', - horarios_detalhados: listaHorarios, - }); } + + setAluno({ + ...data, + perfil: Array.isArray(data.profiles) ? data.profiles[0] : data.profiles, + estagio: Array.isArray(data.estagios) ? data.estagios[0] : data.estagios, + horarios: listaHorarios + }); } catch (err: any) { - console.log('Erro ao carregar aluno:', err.message); + console.log('Erro:', err.message); } finally { setLoading(false); } }; - useEffect(() => { - if (alunoId) fetchAluno(); - }, [alunoId]); + useEffect(() => { if (alunoId) fetchAluno(); }, [alunoId]); - if (loading) { - return ( - - - - ); - } + if (loading) return ( + + + + ); return ( - - + - {/* HEADER */} + {/* HEADER CLEAN */} - router.back()} - > - + router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}> + + + Ficha do Aluno + + - - - Ficha Aluno - - Detalhes acadêmicos e estágio - - - - {/* SECÇÃO PERFIL */} - - - {aluno?.nome.charAt(0).toUpperCase()} + + + {/* PERFIL MINIMALISTA */} + + + {aluno?.nome?.charAt(0)} {aluno?.nome} - {aluno?.turma_curso} + {aluno?.turma_curso} - {/* DADOS PESSOAIS */} + {/* DADOS PESSOAIS CARD */} + + + Linking.openURL(`mailto:${aluno?.perfil?.email}`)} /> + Linking.openURL(`tel:${aluno?.perfil?.telefone}`)} /> + + + + {/* SECÇÃO ESTÁGIO */} - Dados Pessoais + Plano de Estágio - - - - - - + {aluno?.estagio ? ( + + + + ATIVO + + + {aluno?.estagio?.empresas?.nome || 'Empresa Indefinida'} + + + TUTOR NA EMPRESA + {aluno?.estagio?.empresas?.tutor_nome || 'Não definido'} + {aluno?.estagio?.empresas?.tutor_telefone || '-'} + - {/* INFORMAÇÃO DE ESTÁGIO */} - - Estágio Atual - - + + HORÁRIOS + {aluno?.horarios && aluno.horarios.length > 0 ? ( + aluno.horarios.map((h: string, i: number) => ( + {h} + )) + ) : ( + Não registado + )} + - - - - - - - - Horários Registados - {aluno?.horarios_detalhados && aluno.horarios_detalhados.length > 0 ? ( - aluno.horarios_detalhados.map((h, i) => ( - {h} - )) - ) : ( - Não definidos - )} - + + + {/* PERÍODO CORRIGIDO: DUAS COLUNAS COM ÍCONE */} + + + + + INÍCIO + {aluno?.estagio?.data_inicio} + + + + + FIM PREVISTO + {aluno?.estagio?.data_fim} + + + + - - - - - - - - + ) : ( + + + Sem estágio atribuído - + )} ); }); -const DetailRow = ({ icon, label, value, cores, ultimo }: any) => ( - - +const DetailRow = ({ icon, label, value, cores, ultimo, onPress }: any) => ( + + {label} - {value} + {value || '-'} - + {onPress && } + ); const styles = StyleSheet.create({ - safe: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 }, - backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 13, fontWeight: '600' }, - listPadding: { paddingHorizontal: 20, paddingTop: 10 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 15 }, - sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 }, - sectionLine: { flex: 1, height: 1, borderRadius: 1 }, - profileCard: { flexDirection: 'row', alignItems: 'center', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 10 }, - avatarCircle: { width: 60, height: 60, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginRight: 15 }, - avatarTxt: { fontSize: 24, fontWeight: '900' }, - alunoNome: { fontSize: 18, fontWeight: '800' }, - alunoCurso: { fontSize: 14, fontWeight: '600', marginTop: 2 }, - infoCard: { borderRadius: 24, borderWidth: 1, paddingHorizontal: 15, paddingVertical: 5 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 18, fontWeight: '900' }, + btnAction: { width: 44, height: 44, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + scroll: { paddingHorizontal: 24, paddingTop: 10 }, + profileSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 25, gap: 18 }, + avatar: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center' }, + avatarTxt: { fontSize: 26, fontWeight: '900' }, + alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + alunoCurso: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase' }, + infoCard: { borderRadius: 25, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 5, marginBottom: 30 }, row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 15 }, - rowIcon: { marginRight: 15, width: 20 }, rowLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 }, rowValue: { fontSize: 15, fontWeight: '700' }, - datesRow: { flexDirection: 'row' } + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 }, + sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginRight: 15 }, + sectionLine: { flex: 1, height: 1, opacity: 0.3 }, + estagioCard: { padding: 25, borderRadius: 32, elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 }, + estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }, + statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + statusText: { color: '#fff', fontSize: 10, fontWeight: '900' }, + empresaNome: { color: '#fff', fontSize: 24, fontWeight: '900', marginBottom: 20 }, + tutorInfo: { marginBottom: 15 }, + miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, fontWeight: '900', marginBottom: 4 }, + tutorNome: { color: '#fff', fontSize: 16, fontWeight: '800' }, + tutorTel: { color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: '600' }, + horarioBox: { marginBottom: 20, backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15 }, + horarioTxt: { color: '#fff', fontSize: 14, fontWeight: '700' }, + estagioDivider: { height: 1, backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 20 }, + + // ESTILOS DO RODAPÉ CORRIGIDOS + estagioFooter: { flexDirection: 'row', justifyContent: 'space-between' }, + periodoCol: { flex: 1, flexDirection: 'row', alignItems: 'center' }, + footerVal: { color: '#fff', fontSize: 14, fontWeight: '800' }, + + noEstagio: { padding: 30, borderRadius: 25, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center', gap: 10 }, + noEstagioTxt: { fontWeight: '700', fontSize: 14 } }); export default DetalhesAlunos; \ No newline at end of file diff --git a/app/Professor/Alunos/Estagios.tsx b/app/Professor/Alunos/Estagios.tsx index 646e320..867fd69 100644 --- a/app/Professor/Alunos/Estagios.tsx +++ b/app/Professor/Alunos/Estagios.tsx @@ -1,7 +1,9 @@ +// app/Professor/Estagios/index.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useEffect, useMemo, useState } from 'react'; import { + ActivityIndicator, Alert, Modal, ScrollView, @@ -34,32 +36,31 @@ export default function Estagios() { const router = useRouter(); const { isDarkMode } = useTheme(); const insets = useSafeAreaInsets(); - const azulPetroleo = '#2390a6'; + + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', vermelho: '#EF4444', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.5)', + overlay: 'rgba(26, 54, 93, 0.8)', }), [isDarkMode]); - // Estados de Dados + // Estados const [estagios, setEstagios] = useState([]); const [alunos, setAlunos] = useState([]); const [empresas, setEmpresas] = useState([]); const [loading, setLoading] = useState(true); - - // Estados de Modais const [modalVisible, setModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [estagioParaApagar, setEstagioParaApagar] = useState<{id: string, nome: string} | null>(null); - - // Estados de Formulário const [passo, setPasso] = useState(1); const [alunoSelecionado, setAlunoSelecionado] = useState(null); const [empresaSelecionada, setEmpresaSelecionada] = useState(null); @@ -68,13 +69,11 @@ export default function Estagios() { const [dataFim, setDataFim] = useState(''); const [searchMain, setSearchMain] = useState(''); - // Horários const [hManhaIni, setHManhaIni] = useState(''); const [hManhaFim, setHManhaFim] = useState(''); const [hTardeIni, setHTardeIni] = useState(''); const [hTardeFim, setHTardeFim] = useState(''); - // --- Lógica de Dados --- useEffect(() => { fetchDados(); }, []); const fetchDados = async () => { @@ -91,9 +90,25 @@ export default function Estagios() { } catch (e) { console.error(e); } finally { setLoading(false); } }; + const carregarHorariosEdicao = async (estagioId: string) => { + const { data } = await supabase.from('horarios_estagio').select('*').eq('estagio_id', estagioId); + if (data) { + setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim(''); + data.forEach(h => { + if (h.periodo === 'Manhã') { + setHManhaIni(h.hora_inicio?.slice(0, 5) || ''); + setHManhaFim(h.hora_fim?.slice(0, 5) || ''); + } else if (h.periodo === 'Tarde') { + setHTardeIni(h.hora_inicio?.slice(0, 5) || ''); + setHTardeFim(h.hora_fim?.slice(0, 5) || ''); + } + }); + } + }; + const totalHorasDiarias = useMemo(() => { const calcularMinutos = (ini: string, fim: string) => { - if (!ini || !fim) return 0; + if (!ini || !fim || !ini.includes(':') || !fim.includes(':')) return 0; const [hI, mI] = ini.split(':').map(Number); const [hF, mF] = fim.split(':').map(Number); const totalI = (hI || 0) * 60 + (mI || 0); @@ -106,197 +121,207 @@ export default function Estagios() { return m > 0 ? `${h}h${m}m` : `${h}h`; }, [hManhaIni, hManhaFim, hTardeIni, hTardeFim]); - // --- Funções de Ação --- - const prepararApagar = (id: string, nome: string) => { - setEstagioParaApagar({ id, nome }); - setDeleteModalVisible(true); + const formatarHoraPostgres = (time: string) => { + if (!time || !time.includes(':')) return null; + const [h, m] = time.split(':'); + return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`; + }; + + const salvarEstagio = async () => { + setLoading(true); + try { + const { data: { user } } = await supabase.auth.getUser(); + if (empresaSelecionada) { + await supabase.from('empresas').update({ + tutor_nome: empresaSelecionada.tutor_nome, + tutor_telefone: empresaSelecionada.tutor_telefone + }).eq('id', empresaSelecionada.id); + } + + const payload = { + aluno_id: alunoSelecionado?.id, + empresa_id: empresaSelecionada?.id, + professor_id: user?.id, + data_inicio: dataInicio || new Date().toISOString().split('T')[0], + data_fim: dataFim || null, + horas_diarias: totalHorasDiarias, + estado: 'Ativo', + }; + + const { data: estData, error: errE } = editandoEstagio + ? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single() + : await supabase.from('estagios').insert([payload]).select().single(); + + if (errE) throw errE; + const currentId = editandoEstagio?.id || estData.id; + + await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId); + const novosHorarios = []; + if (hManhaIni && hManhaFim) { + novosHorarios.push({ estagio_id: currentId, periodo: 'Manhã', hora_inicio: formatarHoraPostgres(hManhaIni), hora_fim: formatarHoraPostgres(hManhaFim) }); + } + if (hTardeIni && hTardeFim) { + novosHorarios.push({ estagio_id: currentId, periodo: 'Tarde', hora_inicio: formatarHoraPostgres(hTardeIni), hora_fim: formatarHoraPostgres(hTardeFim) }); + } + if (novosHorarios.length > 0) await supabase.from('horarios_estagio').insert(novosHorarios); + + handleFecharModal(); + fetchDados(); + } catch (error: any) { Alert.alert("Erro", error.message); } finally { setLoading(false); } }; const confirmarEliminacao = async () => { if (!estagioParaApagar) return; setLoading(true); const { error } = await supabase.from('estagios').delete().eq('id', estagioParaApagar.id); - if (error) Alert.alert("Erro", "Não foi possível apagar."); - else { - setDeleteModalVisible(false); - setEstagioParaApagar(null); - fetchDados(); - } + if (!error) { setDeleteModalVisible(false); fetchDados(); } setLoading(false); }; - const salvarEstagio = async () => { - setLoading(true); - const { data: { user } } = await supabase.auth.getUser(); - - if (empresaSelecionada) { - await supabase.from('empresas').update({ - tutor_nome: empresaSelecionada.tutor_nome, - tutor_telefone: empresaSelecionada.tutor_telefone - }).eq('id', empresaSelecionada.id); - } - - const payload = { - aluno_id: alunoSelecionado?.id, - empresa_id: empresaSelecionada?.id, - professor_id: user?.id, - data_inicio: dataInicio || new Date().toISOString().split('T')[0], - data_fim: dataFim || null, - horas_diarias: totalHorasDiarias, - estado: 'Ativo', - }; - - const { data: estData, error: errE } = editandoEstagio - ? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single() - : await supabase.from('estagios').insert([payload]).select().single(); - - if (errE) { setLoading(false); return Alert.alert("Erro", errE.message); } - - const currentId = editandoEstagio?.id || estData.id; - await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId); - - const horarios = []; - if (hManhaIni && hManhaFim) horarios.push({ estagio_id: currentId, periodo: 'Manhã', hora_inicio: hManhaIni, hora_fim: hManhaFim }); - if (hTardeIni && hTardeFim) horarios.push({ estagio_id: currentId, periodo: 'Tarde', hora_inicio: hTardeIni, hora_fim: hTardeFim }); - - if (horarios.length > 0) await supabase.from('horarios_estagio').insert(horarios); - - handleFecharModal(); - fetchDados(); - }; - const handleFecharModal = () => { - setModalVisible(false); - setEditandoEstagio(null); - setAlunoSelecionado(null); - setEmpresaSelecionada(null); - setDataInicio(''); setDataFim(''); - setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim(''); + setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); + setDataInicio(''); setDataFim(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim(''); setPasso(1); }; - // --- Filtros --- const estagiosFiltrados = useMemo(() => { const groups: Record = {}; estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => { - const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Sem Turma'; + const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Geral'; if (!groups[chave]) groups[chave] = []; groups[chave].push(e); }); return Object.keys(groups).map(titulo => ({ titulo, dados: groups[titulo] })).sort((a, b) => b.titulo.localeCompare(a.titulo)); }, [estagios, searchMain]); - const alunosAgrupados = useMemo(() => { - const groups: Record = {}; - 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 */} + + {/* HEADER EPVC */} - router.back()}> - + router.back()}> + - Estágios - - + + Plano de Estágios + {estagios.length} Alunos Colocados + + + - + + {/* SEARCH MODERNO */} - - + + - {estagiosFiltrados.map(grupo => ( + {loading ? ( + + ) : estagiosFiltrados.map(grupo => ( - - {grupo.titulo} + + {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 || ''); + carregarHorariosEdicao(e.id); setPasso(2); setModalVisible(true); }} - onLongPress={() => prepararApagar(e.id, e.alunos?.nome)} - > + onLongPress={() => { + setEstagioParaApagar({ id: e.id, nome: e.alunos?.nome }); + setDeleteModalVisible(true); + }} + > {e.alunos?.nome.charAt(0)} {e.alunos?.nome} - {e.alunos?.turma_curso} + + + {e.empresas?.nome} + + + + + {e.horas_diarias} - - - {e.empresas?.nome} - {e.horas_diarias}/dia - - - + ))} ))} - { setPasso(1); setModalVisible(true); }}> - - Novo Estágio + {/* FAB EPVC */} + { setPasso(1); setModalVisible(true); }} + > + + Novo Plano - {/* --- Modal Principal (Cadastro/Edição) --- */} + {/* MODAL DE CRIAÇÃO/EDIÇÃO */} - {passo === 1 ? "Vincular Dados" : "Detalhes do Estágio"} - + + {passo === 1 ? "Vinculação" : "Configuração"} + Passo {passo} de 2 + + + + {passo === 1 ? ( - Estagiário - + Selecionar Estagiário + - {Object.entries(alunosAgrupados).map(([turma, lista]) => ( + {Object.entries(alunos.reduce((acc, a) => { + const key = `${a.ano}º ${a.turma_curso}`.trim().toUpperCase(); + if (!acc[key]) acc[key] = []; acc[key].push(a); return acc; + }, {} as Record)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([turma, lista]) => ( - {turma} + {turma} {lista.map(a => ( - setAlunoSelecionado(a)}> + setAlunoSelecionado(a)} + > {a.nome} + {alunoSelecionado?.id === a.id && } ))} @@ -304,15 +329,25 @@ export default function Estagios() { - Empresa - + Entidade de Acolhimento + - {Object.entries(empresasAgrupadas).map(([curso, lista]) => ( + {Object.entries(empresas.reduce((acc, e) => { + const key = (e.curso || 'GERAL').trim().toUpperCase(); + if (!acc[key]) acc[key] = []; acc[key].push(e); return acc; + }, {} as Record)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([curso, lista]) => ( {curso} {lista.map(emp => ( - setEmpresaSelecionada(emp)}> + setEmpresaSelecionada(emp)} + > {emp.nome} + {empresaSelecionada?.id === emp.id && } ))} @@ -323,69 +358,75 @@ export default function Estagios() { ) : ( - Duração + Cronograma - INÍCIO - FIM + DATA INÍCIO + DATA FIM (PREV.) - Horários + + Horário de Trabalho + {totalHorasDiarias}/dia + 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"/> + Responsável na Empresa + setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholder="Nome do Tutor"/> + setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Telemóvel"/> )} - {passo === 2 && setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.fundo }]}>VOLTAR} + {passo === 2 && ( + setPasso(1)} style={[styles.btnModalSec, { borderColor: cores.borda }]}> + VOLTAR + + )} { if(passo === 1) { if(alunoSelecionado && empresaSelecionada) setPasso(2); - else Alert.alert("Vai dar merda", "Seleciona o aluno e a empresa primeiro!"); + else Alert.alert("Atenção", "Selecione o aluno e a empresa."); } else salvarEstagio(); }} style={[styles.btnModalPri, { backgroundColor: cores.azul }]} > - {passo === 1 ? "PRÓXIMO" : "FINALIZAR"} + {loading ? : {passo === 1 ? "CONTINUAR" : "FINALIZAR"}} - {/* --- NOVO MODAL DE ELIMINAÇÃO MODERNIZADO --- */} + {/* DELETE MODAL */} - - - - + + + + - Eliminar Estágio? + Cancelar Estágio? - Estás prestes a apagar o registo de {estagioParaApagar?.nome}. Esta ação é irreversível. + Irá remover o vínculo de {estagioParaApagar?.nome}. Esta ação é irreversível. setDeleteModalVisible(false)}> - CANCELAR + FECHAR ELIMINAR @@ -399,59 +440,60 @@ export default function Estagios() { } const styles = StyleSheet.create({ - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 }, - headerTitle: { fontSize: 18, fontWeight: '900' }, - btnAction: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - scroll: { paddingHorizontal: 20, paddingTop: 10 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 52, borderRadius: 16, borderWidth: 1, marginBottom: 20 }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15, marginLeft: 5 }, - sectionIndicator: { width: 4, height: 16, borderRadius: 2, marginRight: 8 }, - sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }, - estagioCard: { padding: 16, borderRadius: 24, borderWidth: 1, marginBottom: 12 }, - cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 12 }, - avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, fontWeight: '900' }, - alunoNome: { fontSize: 15, fontWeight: '800' }, - turmaNome: { fontSize: 12, fontWeight: '600' }, - cardDetails: { flexDirection: 'row', gap: 15 }, - detailItem: { flexDirection: 'row', alignItems: 'center', gap: 6, flexShrink: 1 }, - detailText: { fontSize: 12, fontWeight: '700' }, - fab: { position: 'absolute', right: 20, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 15, borderRadius: 20, elevation: 8 }, - fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 8 }, - modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - modalContent: { borderTopLeftRadius: 35, borderTopRightRadius: 35, padding: 25, height: '90%', width: '100%', marginTop: 'auto' }, - modalIndicator: { width: 40, height: 5, backgroundColor: '#E2E8F0', borderRadius: 10, alignSelf: 'center', marginBottom: 15 }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - modalTitle: { fontSize: 20, fontWeight: '900' }, - closeBtn: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 8, marginLeft: 5 }, - pickerContainer: { borderWidth: 1, borderRadius: 20, overflow: 'hidden' }, - groupHeader: { paddingVertical: 8, paddingHorizontal: 15, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.05)' }, - groupHeaderText: { fontSize: 10, fontWeight: '900' }, - pickerItem: { padding: 16, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, - pickerItemText: { fontSize: 14, fontWeight: '600' }, - modernGroup: { padding: 18, borderRadius: 24, borderWidth: 1, marginBottom: 15 }, - groupTitle: { fontSize: 13, fontWeight: '900', color: '#2390a6', marginBottom: 10 }, - rowInputs: { flexDirection: 'row', gap: 12 }, - miniLabel: { fontSize: 10, fontWeight: '800', color: '#94A3B8', marginBottom: 5 }, - modernInput: { paddingVertical: 10, paddingHorizontal: 15, borderRadius: 12, backgroundColor: 'rgba(0,0,0,0.03)', fontSize: 14, fontWeight: '700' }, - tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 }, - tabelaLabel: { flex: 1.2, fontSize: 13, fontWeight: '700' }, - tabelaInput: { flex: 1, padding: 12, borderRadius: 12, backgroundColor: 'rgba(0,0,0,0.04)', textAlign: 'center', fontSize: 14, fontWeight: '700' }, - totalBadge: { alignSelf: 'flex-start', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, marginTop: 5 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + scroll: { paddingHorizontal: 24, paddingTop: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5, marginBottom: 25 }, + searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + estagioCard: { padding: 18, borderRadius: 28, borderWidth: 1, marginBottom: 14, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 14 }, + avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 20, fontWeight: '900' }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + empresaRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, + empresaText: { fontSize: 13, fontWeight: '600' }, + timeBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12, borderWidth: 1 }, + timeText: { fontSize: 11, fontWeight: '900' }, + fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 10 }, + fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase', letterSpacing: 0.5 }, + modalOverlay: { flex: 1, justifyContent: 'flex-end' }, + modalContent: { borderTopLeftRadius: 45, borderTopRightRadius: 45, padding: 28, height: '92%', width: '100%' }, + modalIndicator: { width: 45, height: 5, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, alignSelf: 'center', marginBottom: 20 }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 }, + modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, + modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 }, + closeBtn: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 10, marginLeft: 5, letterSpacing: 0.5 }, + pickerContainer: { borderWidth: 1.5, borderRadius: 24, overflow: 'hidden' }, + groupHeader: { paddingVertical: 10, paddingHorizontal: 18 }, + groupHeaderText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' }, + pickerItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 18, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, + pickerItemText: { fontSize: 15, fontWeight: '700' }, + modernGroup: { padding: 20, borderRadius: 30, borderWidth: 1, marginBottom: 18 }, + groupTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 0.5 }, + rowInputs: { flexDirection: 'row', gap: 15 }, + miniLabel: { fontSize: 9, fontWeight: '900', color: '#94A3B8', marginBottom: 6, marginLeft: 2 }, + modernInput: { paddingVertical: 14, paddingHorizontal: 16, borderRadius: 16, fontSize: 14, fontWeight: '700', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, + tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 }, + tabelaLabel: { flex: 1, fontSize: 14, fontWeight: '800' }, + tabelaInput: { flex: 1, padding: 14, borderRadius: 16, textAlign: 'center', fontSize: 14, fontWeight: '800', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' }, + totalBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, totalText: { color: '#fff', fontSize: 11, fontWeight: '900' }, - modalFooter: { flexDirection: 'row', gap: 15, marginTop: 10 }, - btnModalPri: { flex: 2, height: 58, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - btnModalSec: { flex: 1, height: 58, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - - // Estilos do Delete Modal - deleteCard: { width: '85%', borderRadius: 30, padding: 25, alignItems: 'center' }, - deleteIconBg: { width: 70, height: 70, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - deleteTitle: { fontSize: 20, fontWeight: '900', marginBottom: 10 }, - deleteSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 20, marginBottom: 25 }, - deleteFooter: { flexDirection: 'row', gap: 12, width: '100%' }, + modalFooter: { flexDirection: 'row', gap: 15, marginTop: 15 }, + btnModalPri: { flex: 2, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center' }, + btnModalSec: { flex: 1, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5 }, + deleteCard: { width: '85%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center' }, + deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12 }, + deleteSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 }, + deleteFooter: { flexDirection: 'row', gap: 15, width: '100%' }, deleteBtnCancel: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, - deleteBtnText: { fontSize: 13, fontWeight: '900' }, + deleteBtnText: { fontSize: 14, fontWeight: '900' }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/Faltas.tsx b/app/Professor/Alunos/Faltas.tsx index 3283831..0471657 100644 --- a/app/Professor/Alunos/Faltas.tsx +++ b/app/Professor/Alunos/Faltas.tsx @@ -1,5 +1,6 @@ +// app/(Admin)/FaltasAlunos.tsx import { Ionicons } from '@expo/vector-icons'; -import { useLocalSearchParams, useRouter } from 'expo-router'; +import { useRouter } from 'expo-router'; import { memo, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, @@ -37,7 +38,6 @@ interface Falta { const FaltasAlunos = memo(() => { const { isDarkMode } = useTheme(); const router = useRouter(); - const params = useLocalSearchParams(); const [search, setSearch] = useState(''); const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]); @@ -48,32 +48,23 @@ const FaltasAlunos = memo(() => { const [faltas, setFaltas] = useState([]); const [loadingFaltas, setLoadingFaltas] = useState(false); + const azulPetroleo = '#2390a6'; + const cores = useMemo(() => ({ fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', card: isDarkMode ? '#1A1A1A' : '#FFFFFF', texto: isDarkMode ? '#F8FAFC' : '#1E293B', secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: '#3B82F6', + azul: azulPetroleo, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', vermelho: '#EF4444', verde: '#10B981', - azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); useEffect(() => { fetchAlunos(); }, []); - useEffect(() => { - if (params.alunoId && typeof params.alunoId === 'string') { - const alunoAuto = { - id: params.alunoId, - nome: (params.nome as string) || 'Aluno', - n_escola: '', - turma: '', - }; - abrirFaltas(alunoAuto); - } - }, [params.alunoId]); - const fetchAlunos = async () => { try { setLoading(true); @@ -89,47 +80,85 @@ const FaltasAlunos = memo(() => { data?.forEach(item => { const nomeTurma = `${item.ano}º ${item.turma_curso}`; if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = []; - agrupadas[nomeTurma].push({ id: item.id, nome: item.nome, n_escola: item.n_escola, turma: nomeTurma }); + agrupadas[nomeTurma].push({ + id: item.id, + nome: item.nome, + n_escola: item.n_escola, + turma: nomeTurma + }); }); setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] }))); - } catch (err) { console.error(err); } finally { setLoading(false); } + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } }; const abrirFaltas = async (aluno: Aluno) => { setAlunoSelecionado(aluno); setModalFaltasVisible(true); setLoadingFaltas(true); + setFaltas([]); + try { + const hoje = new Date().toISOString().split('T')[0]; + + // CORREÇÃO DO ERRO PGRST116: Usamos .limit(1) em vez de .single() + const { data: listaEstagios, error: errEstagio } = await supabase + .from('estagios') + .select('data_inicio, data_fim') + .lte('data_inicio', hoje) + .gte('data_fim', hoje) + .limit(1); // Pega apenas o primeiro se houver múltiplos + + if (errEstagio) throw errEstagio; + + // Se não houver estágio hoje, tentamos pegar o mais recente + let inicio = '2026-01-01'; + let fim = '2026-12-31'; + + if (listaEstagios && listaEstagios.length > 0) { + inicio = listaEstagios[0].data_inicio; + fim = listaEstagios[0].data_fim; + } + const { data, error } = await supabase .from('presencas') .select('id, data, justificacao_url, estado') .eq('aluno_id', aluno.id) .eq('estado', 'faltou') + .gte('data', inicio) + .lte('data', fim) .order('data', { ascending: false }); if (error) throw error; setFaltas(data || []); - } catch (err) { console.error(err); } finally { setLoadingFaltas(false); } + + } catch (err) { + console.error("Erro na base:", err); + Alert.alert("Erro", "Não foi possível carregar as faltas deste estágio."); + } finally { + setLoadingFaltas(false); + } }; const verDocumento = async (url: string) => { try { - const supported = await Linking.canOpenURL(url); - if (supported) { - await Linking.openURL(url); - } else { - Alert.alert("Erro", "Não foi possível abrir o link do documento."); - } + await Linking.openURL(url); } catch (error) { - Alert.alert("Erro", "URL inválida ou documento inacessível."); + Alert.alert("Erro", "Não foi possível abrir o comprovativo."); } }; const filteredTurmas = turmas .map(turma => ({ ...turma, - alunos: turma.alunos.filter(a => a.nome.toLowerCase().includes(search.toLowerCase()) || a.n_escola.includes(search)), + alunos: turma.alunos.filter(a => + a.nome.toLowerCase().includes(search.toLowerCase()) || + a.n_escola.includes(search) + ), })) .filter(t => t.alunos.length > 0); @@ -139,20 +168,17 @@ const FaltasAlunos = memo(() => { - router.back()} - style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]} - > - + router.back()} style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}> + - Gestão de Faltas - + Gestão de Faltas + - - + + { showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - - {item.nome} - - + {item.nome} {item.alunos.map(aluno => ( - abrirFaltas(aluno)} - > - - {aluno.nome.charAt(0)} + abrirFaltas(aluno)}> + + {aluno.nome.charAt(0).toUpperCase()} {aluno.nome} - Nº {aluno.n_escola} + Nº Aluno: {aluno.n_escola} + + + - ))} @@ -199,24 +219,16 @@ const FaltasAlunos = memo(() => { /> )} - setModalFaltasVisible(false)} - > + setModalFaltasVisible(false)}> - - + + - Histórico de Faltas - {alunoSelecionado?.nome} + Faltas de Estágio + {alunoSelecionado?.nome} - setModalFaltasVisible(false)} - style={[styles.closeBtn, { backgroundColor: cores.fundo }]} - > - + setModalFaltasVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> + @@ -226,41 +238,32 @@ const FaltasAlunos = memo(() => { {faltas.length === 0 ? ( - - Nenhuma falta registada. + + + + Sem faltas no período atual ) : ( faltas.map(f => ( - - - - - - {new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })} - + + + + - - + + + {new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })} + + {f.justificacao_url ? 'Justificada' : 'Injustificada'} + {f.justificacao_url && ( + verDocumento(f.justificacao_url!)}> + + + )} - - {f.justificacao_url ? ( - verDocumento(f.justificacao_url!)} - > - - Ver Comprovativo - - ) : ( - - - Aguardando justificação - - )} )) )} @@ -276,44 +279,37 @@ const FaltasAlunos = memo(() => { export default FaltasAlunos; const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 }, - headerFixed: { paddingHorizontal: 20, paddingBottom: 15 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 70 }, - backBtnPremium: { - width: 42, height: 42, borderRadius: 14, - justifyContent: 'center', alignItems: 'center', - borderWidth: 1, elevation: 2, shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2 - }, - title: { fontSize: 22, fontWeight: '800' }, - searchBox: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 15, paddingHorizontal: 15, height: 50, marginTop: 10 }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 30 }, + safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, + headerFixed: { paddingHorizontal: 20, paddingBottom: 10 }, + topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', height: 60, marginTop: 10 }, + backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + topTitle: { fontSize: 18, fontWeight: '800' }, + searchContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 16, borderWidth: 1.5, marginTop: 10 }, + searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 30, paddingTop: 10 }, section: { marginBottom: 25 }, - turmaBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 }, - turmaLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.8 }, - card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 22, marginBottom: 10, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 }, - avatar: { width: 46, height: 46, borderRadius: 15, justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, fontWeight: '800' }, + sectionLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 5, letterSpacing: 1.2 }, + card: { flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 24, marginBottom: 12, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, + avatar: { width: 50, height: 50, borderRadius: 18, alignItems: 'center', justifyContent: 'center' }, + avatarText: { color: '#fff', fontSize: 22, fontWeight: '800' }, info: { flex: 1, marginLeft: 15 }, nome: { fontSize: 16, fontWeight: '700' }, - subText: { fontSize: 12, marginTop: 2, fontWeight: '500' }, + subText: { fontSize: 12, fontWeight: '600', marginTop: 3 }, + alertIcon: { width: 34, height: 34, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, - modalContent: { height: '85%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, borderBottomWidth: 1 }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' }, + modalContent: { height: '82%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25 }, modalTitle: { fontSize: 20, fontWeight: '800' }, - modalSubtitle: { fontSize: 14, fontWeight: '600', marginTop: 2 }, - closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - faltaCard: { padding: 16, borderRadius: 20, marginBottom: 15, borderWidth: 1 }, - faltaHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }, - faltaData: { fontSize: 15, fontWeight: '700' }, - statusBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, - statusText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' }, - pdfBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: 12, padding: 12 }, - pdfBtnText: { marginLeft: 10, fontSize: 14, fontWeight: '700' }, - noJustifContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 5 }, - noJustifText: { marginLeft: 6, fontSize: 12, fontWeight: '600', fontStyle: 'italic' }, - emptyState: { alignItems: 'center', marginTop: 50 }, - emptyText: { textAlign: 'center', marginTop: 15, fontSize: 14, fontWeight: '600' } + modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 }, + closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + faltaCard: { padding: 16, borderRadius: 22, marginBottom: 12, elevation: 1 }, + faltaRow: { flexDirection: 'row', alignItems: 'center' }, + dateBadge: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + faltaData: { fontSize: 15, fontWeight: '800' }, + faltaStatus: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 }, + viewBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + emptyState: { alignItems: 'center', marginTop: 80 }, + emptyIconContainer: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + emptyText: { fontSize: 16, fontWeight: '700' } }); \ No newline at end of file diff --git a/app/Professor/Alunos/ListaAlunos.tsx b/app/Professor/Alunos/ListaAlunos.tsx index 89495d8..6bb58ed 100644 --- a/app/Professor/Alunos/ListaAlunos.tsx +++ b/app/Professor/Alunos/ListaAlunos.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'expo-router'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, + Alert, FlatList, RefreshControl, StatusBar, @@ -23,6 +24,7 @@ export interface Aluno { nome: string; n_escola: string; turma: string; + tem_estagio?: boolean; } interface TurmaAgrupada { @@ -40,16 +42,19 @@ const ListaAlunosProfessor = memo(() => { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const azulPetroleo = '#2390a6'; + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + verde: '#10B981', }), [isDarkMode]); const fetchAlunos = async () => { @@ -57,7 +62,14 @@ const ListaAlunosProfessor = memo(() => { setLoading(true); const { data, error } = await supabase .from('alunos') - .select('id, nome, n_escola, ano, turma_curso') + .select(` + id, + nome, + n_escola, + ano, + turma_curso, + estagios(id) + `) .order('ano', { ascending: false }) .order('nome', { ascending: true }); @@ -66,17 +78,21 @@ const ListaAlunosProfessor = memo(() => { const agrupadas: Record = {}; data.forEach(item => { - const nomeTurma = `${item.ano}º ${item.turma_curso}`; + const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase(); if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = []; agrupadas[nomeTurma].push({ id: item.id, nome: item.nome, n_escola: item.n_escola, turma: nomeTurma, + tem_estagio: item.estagios && item.estagios.length > 0 }); }); - setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] }))); + setTurmas(Object.keys(agrupadas) + .sort((a, b) => b.localeCompare(a)) + .map(nome => ({ nome, alunos: agrupadas[nome] })) + ); } catch (err) { console.error('Erro:', err); } finally { @@ -110,37 +126,35 @@ const ListaAlunosProfessor = memo(() => { - {/* HEADER IGUAL ÀS EMPRESAS */} + {/* HEADER EPVC STYLE */} router.back()} > - + - + Alunos - - Gestão de turmas e estágios - + Gestão de Turmas - + - {/* SEARCH BAR IGUAL ÀS EMPRESAS */} + {/* SEARCH MODERNO */} - + { {loading && !refreshing ? ( - - - + ) : ( item.nome} - contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]} + contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 100 }]} refreshControl={} renderItem={({ item }) => ( - {/* Cabeçalho da Secção (Turma) */} - {item.nome} + + {item.nome} @@ -176,19 +188,31 @@ const ListaAlunosProfessor = memo(() => { params: { alunoId: aluno.id } })} > - - + + + {aluno.nome.charAt(0)} + {aluno.nome} - - Nº Escola: {aluno.n_escola} + + Nº {aluno.n_escola} - + {aluno.tem_estagio ? ( + + + COLOCADO + + ) : ( + + + PENDENTE + + )} ))} @@ -196,11 +220,20 @@ const ListaAlunosProfessor = memo(() => { ListEmptyComponent={() => ( - Nenhum aluno encontrado. + Nenhum aluno encontrado. )} /> )} + + {/* FAB */} + Alert.alert("EPVC", "Funcionalidade de registo de aluno em desenvolvimento.")} + > + + Novo Aluno + ); @@ -208,26 +241,30 @@ const ListaAlunosProfessor = memo(() => { const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 }, - backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 13, fontWeight: '600' }, - refreshBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 5 }, - searchSection: { paddingHorizontal: 20, marginVertical: 15 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 18, borderWidth: 1.5 }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' }, - loadingCenter: { marginTop: 50, alignItems: 'center' }, - emptyContainer: { marginTop: 80, alignItems: 'center' }, - listPadding: { paddingHorizontal: 20 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 15 }, - sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 }, - sectionLine: { flex: 1, height: 1, borderRadius: 1 }, - alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 }, - alunoIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + searchSection: { paddingHorizontal: 24, marginBottom: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, + searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' }, + listPadding: { paddingHorizontal: 24, paddingTop: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 }, + avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 18, fontWeight: '900' }, alunoInfo: { flex: 1, marginLeft: 15 }, - alunoNome: { fontSize: 16, fontWeight: '800' }, - idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 }, + alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 }, + idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 }, idText: { fontSize: 13, fontWeight: '600' }, + statusBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10 }, + statusText: { fontSize: 9, fontWeight: '900' }, + emptyContainer: { marginTop: 80, alignItems: 'center' }, + fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 }, + fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase' }, }); export default ListaAlunosProfessor; \ No newline at end of file diff --git a/app/Professor/Alunos/Presencas.tsx b/app/Professor/Alunos/Presencas.tsx index 9267ad7..ff038b1 100644 --- a/app/Professor/Alunos/Presencas.tsx +++ b/app/Professor/Alunos/Presencas.tsx @@ -1,3 +1,4 @@ +// app/(Professor)/Presencas.tsx import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { memo, useEffect, useMemo, useState } from 'react'; @@ -31,14 +32,16 @@ const Presencas = memo(() => { const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]); const [loading, setLoading] = useState(true); + // Paleta de Cores Premium const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: '#3B82F6', - azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)', - borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#FFFFFF' : '#1A1C1E', + secundario: isDarkMode ? '#8E8E93' : '#6C757D', + azul: '#2390a6', // O teu azul de estimação + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + borda: isDarkMode ? '#2C2C2E' : '#E9ECEF', + sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)', }), [isDarkMode]); useEffect(() => { @@ -100,54 +103,65 @@ const Presencas = memo(() => { - {/* HEADER PREMIUM */} - - + {/* HEADER DINÂMICO */} + + router.back()} - style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]} > - + - - Presenças - + + Presenças + Lista de Alunos + + - - + + + {search !== '' && ( + setSearch('')}> + + + )} {loading ? ( - + + A carregar turma... ) : ( item.nome} - contentContainerStyle={styles.scrollContent} + contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - - - - {item.nome} • {item.alunos.length} Alunos - + + + + {item.nome} + + {item.alunos.length} + {item.alunos.map(aluno => ( router.push({ pathname: '/Professor/Alunos/CalendarioPresencas', @@ -155,18 +169,23 @@ const Presencas = memo(() => { }) } > - - + + {aluno.nome.charAt(0).toUpperCase()} - - {aluno.nome} - Ver registo de presenças + + {aluno.nome} + + + Registo de faltas + - + + + ))} @@ -184,77 +203,83 @@ const styles = StyleSheet.create({ flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0, }, - headerFixed: { - paddingHorizontal: 20, - paddingBottom: 15, + headerContainer: { + paddingHorizontal: 24, + paddingBottom: 20, + paddingTop: 10, }, - topBar: { + topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - height: 70, + marginBottom: 20, }, - backBtnPremium: { - width: 42, - height: 42, - borderRadius: 14, + btnBack: { + width: 45, + height: 45, + borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1, - elevation: 2, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.1, - shadowRadius: 2, }, - title: { fontSize: 22, fontWeight: '800' }, - searchBox: { + titleWrapper: { alignItems: 'center' }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 }, + searchWrapper: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, - borderRadius: 15, - paddingHorizontal: 15, - height: 50, - marginTop: 10, + borderRadius: 18, + paddingHorizontal: 16, + height: 54, }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 30 }, - section: { marginBottom: 25 }, - turmaBadge: { - paddingHorizontal: 12, - paddingVertical: 6, + inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' }, + listContent: { paddingHorizontal: 24, paddingBottom: 40 }, + turmaSection: { marginBottom: 30 }, + turmaHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + paddingLeft: 4 + }, + turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 }, + turmaTitle: { fontSize: 16, fontWeight: '800', flex: 1 }, + countBadge: { + paddingHorizontal: 10, + paddingVertical: 4, borderRadius: 10, - alignSelf: 'flex-start', - marginBottom: 12, }, - turmaLabel: { - fontSize: 12, - fontWeight: '800', - textTransform: 'uppercase', - letterSpacing: 0.8 - }, - card: { + countText: { fontSize: 11, fontWeight: '900' }, + alunoCard: { flexDirection: 'row', alignItems: 'center', - padding: 14, - borderRadius: 22, - marginBottom: 10, - elevation: 3, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.04, - shadowRadius: 10, + padding: 12, + borderRadius: 24, + marginBottom: 12, + elevation: 4, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 1, + shadowRadius: 12, }, - avatar: { - width: 46, - height: 46, - borderRadius: 15, + avatarBox: { + width: 52, + height: 52, + borderRadius: 18, justifyContent: 'center', alignItems: 'center', }, - avatarText: { fontSize: 18, fontWeight: '800' }, - info: { flex: 1, marginLeft: 15 }, - nome: { fontSize: 16, fontWeight: '700' }, - subText: { fontSize: 12, marginTop: 2, fontWeight: '500' }, - centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + avatarChar: { fontSize: 20, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 16 }, + alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 }, + alunoStatusRow: { flexDirection: 'row', alignItems: 'center' }, + alunoSub: { fontSize: 13, fontWeight: '500' }, + goCircle: { + width: 32, + height: 32, + borderRadius: 16, + justifyContent: 'center', + alignItems: 'center', + }, + loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + loaderText: { marginTop: 15, fontSize: 14, fontWeight: '600' }, }); \ No newline at end of file diff --git a/app/Professor/Alunos/Sumarios.tsx b/app/Professor/Alunos/Sumarios.tsx index 2115f81..278c2ad 100644 --- a/app/Professor/Alunos/Sumarios.tsx +++ b/app/Professor/Alunos/Sumarios.tsx @@ -1,3 +1,4 @@ +// app/(Professor)/SumariosAlunos.tsx import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { memo, useEffect, useMemo, useState } from 'react'; @@ -46,18 +47,17 @@ const SumariosAlunos = memo(() => { const [loadingSumarios, setLoadingSumarios] = useState(false); const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: '#3B82F6', - azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)', - borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9', + card: isDarkMode ? '#161618' : '#FFFFFF', + texto: isDarkMode ? '#FFFFFF' : '#1A1C1E', + secundario: isDarkMode ? '#8E8E93' : '#6C757D', + azul: '#2390a6', + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + borda: isDarkMode ? '#2C2C2E' : '#E9ECEF', + sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)', }), [isDarkMode]); - useEffect(() => { - fetchAlunos(); - }, []); + useEffect(() => { fetchAlunos(); }, []); useEffect(() => { if (params.alunoId && typeof params.alunoId === 'string') { @@ -95,11 +95,7 @@ const SumariosAlunos = memo(() => { }); setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] }))); - } catch (err) { - console.error('Erro ao carregar alunos:', err); - } finally { - setLoading(false); - } + } catch (err) { console.error(err); } finally { setLoading(false); } }; const abrirSumarios = async (aluno: Aluno) => { @@ -107,20 +103,37 @@ const SumariosAlunos = memo(() => { setModalVisible(true); setLoadingSumarios(true); try { + const hoje = new Date().toISOString().split('T')[0]; + + // 1. Procurar o estágio ativo + const { data: listaEstagios } = await supabase + .from('estagios') + .select('data_inicio, data_fim') + .lte('data_inicio', hoje) + .gte('data_fim', hoje) + .limit(1); + + let inicio = '2026-01-01'; + let fim = '2026-12-31'; + + if (listaEstagios && listaEstagios.length > 0) { + inicio = listaEstagios[0].data_inicio; + fim = listaEstagios[0].data_fim; + } + + // 2. Buscar sumários filtrados pelo período do estágio const { data, error } = await supabase .from('presencas') .select('id, data, sumario') .eq('aluno_id', aluno.id) + .gte('data', inicio) + .lte('data', fim) .not('sumario', 'is', null) .order('data', { ascending: false }); if (error) throw error; setSumarios(data || []); - } catch (err) { - console.error('Erro sumários:', err); - } finally { - setLoadingSumarios(false); - } + } catch (err) { console.error(err); } finally { setLoadingSumarios(false); } }; const filteredTurmas = turmas @@ -136,61 +149,67 @@ const SumariosAlunos = memo(() => { - - + + router.back()} - style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]} + style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]} > - + - Sumários - + + Sumários + Estágio Atual + + - - + + {loading ? ( - + ) : ( item.nome} - contentContainerStyle={styles.scrollContent} + contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - - - {item.nome} + + + + {item.nome} {item.alunos.map(aluno => ( abrirSumarios(aluno)} > - - {aluno.nome.charAt(0)} + + {aluno.nome.charAt(0).toUpperCase()} - - {aluno.nome} - Nº {aluno.n_escola} + + {aluno.nome} + Nº {aluno.n_escola} + + + - ))} @@ -198,46 +217,45 @@ const SumariosAlunos = memo(() => { /> )} - setModalVisible(false)} - > + setModalVisible(false)}> - - + + - Sumários - {alunoSelecionado?.nome} + Caderno de Sumários + {alunoSelecionado?.nome} - setModalVisible(false)} - style={[styles.closeBtn, { backgroundColor: cores.fundo }]} - > - + setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}> + {loadingSumarios ? ( ) : ( - + {sumarios.length === 0 ? ( - - - Sem registos de sumários. + + + + + Sem sumários neste estágio + Não foram encontrados registos para o período atual. ) : ( sumarios.map(s => ( - - - - - {new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })} - + + + + + {new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })} + + + + + + {s.sumario} - {s.sumario} )) )} @@ -254,38 +272,43 @@ export default SumariosAlunos; const styles = StyleSheet.create({ safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 }, - headerFixed: { paddingHorizontal: 20, paddingBottom: 15 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 70 }, - backBtnPremium: { - width: 42, height: 42, borderRadius: 14, - justifyContent: 'center', alignItems: 'center', - borderWidth: 1, elevation: 2, shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2 - }, - title: { fontSize: 22, fontWeight: '800' }, - searchBox: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 15, paddingHorizontal: 15, height: 50, marginTop: 10 }, - searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 30 }, - section: { marginBottom: 25 }, - turmaBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 }, - turmaLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.8 }, - card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 22, marginBottom: 10, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 }, - avatar: { width: 46, height: 46, borderRadius: 15, justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, fontWeight: '800' }, - info: { flex: 1, marginLeft: 15 }, - nome: { fontSize: 16, fontWeight: '700' }, - subText: { fontSize: 12, marginTop: 2, fontWeight: '500' }, - centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, - modalContent: { height: '85%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, borderBottomWidth: 1 }, - modalTitle: { fontSize: 20, fontWeight: '800' }, - modalSubtitle: { fontSize: 14, fontWeight: '600', marginTop: 2 }, - closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - sumarioCard: { padding: 16, borderRadius: 18, marginBottom: 15, borderWidth: 1 }, - sumarioHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 }, - sumarioData: { fontSize: 13, fontWeight: '800', marginLeft: 6 }, - sumarioTexto: { fontSize: 15, lineHeight: 22, fontWeight: '500' }, - emptyState: { alignItems: 'center', marginTop: 50 }, - emptyText: { textAlign: 'center', marginTop: 15, fontSize: 14, fontWeight: '600' } + headerContainer: { paddingHorizontal: 24, paddingBottom: 20, paddingTop: 10 }, + topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, + btnBack: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + titleWrapper: { alignItems: 'center' }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 }, + searchWrapper: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 18, paddingHorizontal: 16, height: 54 }, + inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' }, + listContent: { paddingHorizontal: 24, paddingBottom: 40 }, + turmaSection: { marginBottom: 30 }, + turmaHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, paddingLeft: 4 }, + turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 }, + turmaTitle: { fontSize: 16, fontWeight: '800' }, + alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 24, marginBottom: 12, elevation: 4, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 1, shadowRadius: 12 }, + avatarBox: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + avatarChar: { fontSize: 20, fontWeight: '900' }, + alunoInfo: { flex: 1, marginLeft: 16 }, + alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 }, + alunoSub: { fontSize: 13, fontWeight: '500' }, + goCircle: { width: 32, height: 32, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'flex-end' }, + modalContent: { height: '88%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, elevation: 2 }, + modalTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 }, + closeBtn: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + modalScroll: { padding: 20, paddingBottom: 50 }, + sumarioCard: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 1, shadowRadius: 6 }, + sumarioHeaderRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 }, + dateBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 }, + dateText: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase' }, + sumarioLine: { flex: 1, height: 1, backgroundColor: 'rgba(128,128,128,0.1)', marginLeft: 15 }, + sumarioBody: { paddingLeft: 4 }, + sumarioTexto: { fontSize: 15, lineHeight: 24, fontWeight: '500', letterSpacing: 0.2 }, + emptyContainer: { alignItems: 'center', marginTop: 100 }, + emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + emptyText: { fontSize: 16, fontWeight: '700' }, + emptySub: { fontSize: 13, textAlign: 'center', marginTop: 8, opacity: 0.7, paddingHorizontal: 30 } }); \ No newline at end of file diff --git a/app/Professor/Empresas/DetalhesEmpresa.tsx b/app/Professor/Empresas/DetalhesEmpresa.tsx index 2983862..2ff65fc 100644 --- a/app/Professor/Empresas/DetalhesEmpresa.tsx +++ b/app/Professor/Empresas/DetalhesEmpresa.tsx @@ -34,7 +34,8 @@ const DetalhesEmpresa = memo(() => { const params = useLocalSearchParams(); const insets = useSafeAreaInsets(); - const azulPetroleo = '#2390a6'; + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const empresaOriginal: Empresa = useMemo(() => { if (!params.empresa) return {} as Empresa; @@ -55,16 +56,17 @@ const DetalhesEmpresa = memo(() => { const [showDeleteModal, setShowDeleteModal] = useState(false); const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', vermelho: '#EF4444', - vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + vermelhoSuave: '#FFF5F5', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.4)', + overlay: 'rgba(26, 54, 93, 0.8)', }), [isDarkMode]); useEffect(() => { @@ -119,7 +121,6 @@ const DetalhesEmpresa = memo(() => { router.back(); } catch (e) { setShowDeleteModal(false); - // Se tiver alunos ligados, vai dar merda ao apagar alert('Não é possível apagar empresas com estágios ativos.'); } finally { setLoading(false); @@ -130,29 +131,27 @@ const DetalhesEmpresa = memo(() => { - {/* FEEDBACK TOAST */} {showSuccess && ( - Alterações guardadas com sucesso! + Dados atualizados! )} - {/* MODAL DE APAGAR MODERNO */} - + - Tens a certeza? + Eliminar Entidade? - Esta ação irá remover permanentemente a entidade {empresaLocal.nome}. + Esta ação é irreversível. A empresa {empresaLocal.nome} será removida do sistema. setShowDeleteModal(false)} > Cancelar @@ -162,7 +161,7 @@ const DetalhesEmpresa = memo(() => { style={[styles.modalBtn, { backgroundColor: cores.vermelho }]} onPress={confirmDelete} > - {loading ? : Apagar} + {loading ? : Confirmar} @@ -172,77 +171,84 @@ const DetalhesEmpresa = memo(() => { router.back()} > - + - Detalhes da Entidade + + Ficha Técnica + ID: #{empresaLocal.id?.slice(0, 5)} + { if(editando) setEmpresaLocal({...empresaOriginal}); setEditando(!editando); }} > - + - + - - Dados Gerais + + Informação Institucional - setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} /> - setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} /> - setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline /> + setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} icon="business-outline" /> + setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} icon="school-outline" /> + setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline icon="map-outline" /> - - Contacto Tutor + + Gestão de Contacto - setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} /> - setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" /> + setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} icon="person-outline" /> + setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" icon="call-outline" /> - - Alunos Ativos - - {loadingAlunos ? '...' : alunos.length} + + Alunos em Estágio + + {loadingAlunos ? '-' : alunos.length} {loadingAlunos ? : alunos.length > 0 ? ( alunos.map((aluno, i) => ( - {aluno.nome.charAt(0).toUpperCase()} + {aluno.nome} + )) - ) : Sem alunos vinculados.} + ) : Nenhum aluno vinculado a esta entidade.} - + {editando ? ( {loading ? : ( - Guardar Alterações + + + Confirmar Alterações + )} ) : ( - setShowDeleteModal(true)}> - - Eliminar Entidade + setShowDeleteModal(true)}> + + Remover Parceiro )} @@ -252,14 +258,22 @@ const DetalhesEmpresa = memo(() => { ); }); -const ModernField = ({ label, value, editable, cores, ...props }: any) => ( +const ModernField = ({ label, value, editable, cores, icon, ...props }: any) => ( - {label} + + + {label} + {editable ? ( - + ) : ( - {value || '---'} + {value || 'Não definido'} )} @@ -267,41 +281,43 @@ const ModernField = ({ label, value, editable, cores, ...props }: any) => ( const styles = StyleSheet.create({ safe: { flex: 1 }, - toast: { position: 'absolute', left: 20, right: 20, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 16, gap: 10, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10 }, - toastText: { color: '#fff', fontSize: 14, fontWeight: '800' }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 }, - headerTitle: { fontSize: 18, fontWeight: '900' }, - btnAction: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - scroll: { paddingHorizontal: 20, paddingTop: 10 }, - card: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, - sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginLeft: 8, flex: 1 }, - fieldContainer: { marginBottom: 15 }, - fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 5, marginLeft: 2 }, - input: { fontSize: 14, fontWeight: '700', paddingHorizontal: 12, paddingVertical: 10, borderRadius: 12, borderWidth: 1.5 }, - readOnly: { paddingHorizontal: 12, paddingVertical: 10, borderRadius: 12, borderWidth: 1 }, - readOnlyTxt: { fontSize: 14, fontWeight: '700' }, - badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6 }, - badgeTxt: { fontSize: 12, fontWeight: '900' }, - alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 }, - miniAvatar: { width: 30, height: 30, borderRadius: 8, justifyContent: 'center', alignItems: 'center', marginRight: 10 }, - miniAvatarTxt: { fontSize: 13, fontWeight: '900' }, - alunoName: { fontSize: 14, fontWeight: '700' }, - empty: { textAlign: 'center', fontSize: 12, fontWeight: '600', paddingVertical: 5 }, - btnSave: { height: 54, borderRadius: 16, justifyContent: 'center', alignItems: 'center', marginTop: 10 }, - btnRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, - btnSaveTxt: { color: '#fff', fontSize: 15, fontWeight: '900', textTransform: 'uppercase' }, - btnDel: { height: 54, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8 }, - btnDelTxt: { fontSize: 15, fontWeight: '800' }, - // Estilos do Modal - modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, - modalContent: { width: '100%', maxWidth: 340, borderRadius: 32, padding: 24, alignItems: 'center', borderWidth: 1, elevation: 20, shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 15 }, - iconCircle: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, - modalTitle: { fontSize: 22, fontWeight: '900', marginBottom: 10 }, - modalSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 20, marginBottom: 25 }, - modalButtons: { flexDirection: 'row', gap: 12, width: '100%' }, - modalBtn: { flex: 1, height: 50, borderRadius: 15, justifyContent: 'center', alignItems: 'center' }, - modalBtnTxt: { fontSize: 14, fontWeight: '800' } + toast: { position: 'absolute', left: 25, right: 25, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, gap: 12, elevation: 8 }, + toastText: { color: '#fff', fontSize: 14, fontWeight: '900' }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }, + headerSubtitle: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, + btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + scroll: { paddingHorizontal: 24, paddingTop: 10 }, + card: { padding: 22, borderRadius: 32, borderWidth: 1, marginBottom: 18, elevation: 2, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, gap: 10 }, + sideLine: { width: 4, height: 18, borderRadius: 2 }, + sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + fieldContainer: { marginBottom: 18 }, + labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 6, marginLeft: 4 }, + fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + input: { fontSize: 15, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 16, borderWidth: 1.5 }, + readOnly: { paddingHorizontal: 16, paddingVertical: 14, borderRadius: 16, borderWidth: 1 }, + readOnlyTxt: { fontSize: 15, fontWeight: '700' }, + countBadge: { width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' }, + countBadgeTxt: { color: '#fff', fontSize: 11, fontWeight: '900' }, + alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14 }, + miniAvatar: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 12 }, + alunoName: { fontSize: 15, fontWeight: '700', flex: 1 }, + empty: { textAlign: 'center', fontSize: 13, fontWeight: '600', paddingVertical: 10, fontStyle: 'italic' }, + footerActions: { marginTop: 10 }, + btnSave: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', elevation: 5, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 }, + btnRow: { flexDirection: 'row', alignItems: 'center', gap: 10 }, + btnSaveTxt: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }, + btnDel: { height: 56, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 1.5, borderStyle: 'dashed' }, + btnDelTxt: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }, + modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 25 }, + modalContent: { width: '100%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1 }, + iconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 }, + modalTitle: { fontSize: 24, fontWeight: '900', marginBottom: 12 }, + modalSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 }, + modalButtons: { flexDirection: 'row', gap: 15 }, + modalBtn: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + modalBtnTxt: { fontSize: 15, fontWeight: '900' } }); export default DetalhesEmpresa; \ No newline at end of file diff --git a/app/Professor/Empresas/ListaEmpresas.tsx b/app/Professor/Empresas/ListaEmpresas.tsx index 750938a..963c84c 100644 --- a/app/Professor/Empresas/ListaEmpresas.tsx +++ b/app/Professor/Empresas/ListaEmpresas.tsx @@ -42,20 +42,21 @@ const ListaEmpresasProfessor = memo(() => { const [refreshing, setRefreshing] = useState(false); const [modalVisible, setModalVisible] = useState(false); - // Estados do Formulário const [form, setForm] = useState({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' }); - const azulPetroleo = '#2390a6'; + // Cores EPVC + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - branco: '#FFFFFF' }), [isDarkMode]); const fetchEmpresas = async () => { @@ -90,7 +91,7 @@ const ListaEmpresasProfessor = memo(() => { ); const grupos = filtradas.reduce((acc: { [key: string]: Empresa[] }, empresa) => { - const cursoKey = (empresa.curso || 'Outros').trim().toUpperCase(); + const cursoKey = (empresa.curso || 'Geral').trim().toUpperCase(); if (!acc[cursoKey]) acc[cursoKey] = []; acc[cursoKey].push(empresa); return acc; @@ -104,7 +105,7 @@ const ListaEmpresasProfessor = memo(() => { const criarEmpresa = async () => { if (!form.nome || !form.morada || !form.tutorNome || !form.tutorTelefone || !form.curso) { - Alert.alert('Atenção', 'Preenche todos os campos obrigatórios.'); + Alert.alert('Atenção', 'Preencha todos os campos.'); return; } @@ -125,7 +126,7 @@ const ListaEmpresasProfessor = memo(() => { setEmpresas(prev => [...prev, data![0]]); setModalVisible(false); setForm({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' }); - Alert.alert('Sucesso', 'Entidade registada!'); + Alert.alert('Sucesso', 'Empresa registada com sucesso!'); } catch (error: any) { Alert.alert('Erro', error.message); } finally { @@ -139,37 +140,37 @@ const ListaEmpresasProfessor = memo(() => { - {/* HEADER MODERNIZADO */} + {/* HEADER EPVC */} router.back()} > - + - Empresas - - {empresas.length} entidades ativas + Entidades + + {empresas.length} parcerias ativas setModalVisible(true)} > - + - {/* SEARCH BAR */} + {/* PESQUISA */} - + { refreshControl={} renderSectionHeader={({ section: { title } }) => ( - {title} + + {title} )} @@ -201,56 +203,58 @@ const ListaEmpresasProfessor = memo(() => { onPress={() => router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', params: { empresa: JSON.stringify(item) } })} > - + {item.nome} - - {item.tutor_nome} + + {item.morada} - + + + )} ListEmptyComponent={() => ( - - Sem resultados. + + Nenhuma entidade encontrada. )} /> )} - {/* MODAL DE CRIAÇÃO PREMIUM */} + {/* MODAL NOVO REGISTO */} - Nova Empresa - Preenche os detalhes da entidade + Registar Parceiro + Nova entidade de estágio 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} /> + + setForm({...form, nome:v})} cores={cores} /> + setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI, MULT, etc." /> + setForm({...form, morada:v})} cores={cores} /> - + - setForm({...form, tutorNome:v})} cores={cores} /> - setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} /> + setForm({...form, tutorNome:v})} cores={cores} /> + setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} /> - Registar Entidade + Confirmar Registo @@ -272,39 +276,41 @@ const ModernInput = ({ label, icon, cores, ...props }: any) => ( const styles = StyleSheet.create({ safe: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 }, - backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, - headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, - headerSubtitle: { fontSize: 13, fontWeight: '600' }, - addBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 5 }, - searchSection: { paddingHorizontal: 20, marginVertical: 15 }, - searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 18, borderWidth: 1.5 }, + header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15 }, + backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + headerTitle: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8 }, + headerSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + addBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 8 }, + searchSection: { paddingHorizontal: 24, marginVertical: 15 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 }, searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' }, - loadingCenter: { marginTop: 50, alignItems: 'center' }, - emptyContainer: { marginTop: 80, alignItems: 'center' }, - listPadding: { paddingHorizontal: 20 }, - sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 15 }, - sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 }, - sectionLine: { flex: 1, height: 1, borderRadius: 1 }, - empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 }, - empresaIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, + loadingCenter: { marginTop: 60, alignItems: 'center' }, + emptyContainer: { marginTop: 100, alignItems: 'center' }, + listPadding: { paddingHorizontal: 24 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 30, marginBottom: 15 }, + sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 }, + sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }, + sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 }, + empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 28, marginBottom: 14, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 12 }, + empresaIcon: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' }, empresaInfo: { flex: 1, marginLeft: 15 }, - empresaNome: { fontSize: 16, fontWeight: '800' }, - tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 }, - tutorText: { fontSize: 13, fontWeight: '600' }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' }, - modalContent: { borderTopLeftRadius: 35, borderTopRightRadius: 35, padding: 25, maxHeight: '85%' }, - modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 }, - modalTitle: { fontSize: 22, fontWeight: '900' }, - modalSub: { fontSize: 14, fontWeight: '600', marginTop: 2 }, - closeBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' }, - divider: { height: 1, backgroundColor: 'rgba(0,0,0,0.05)', marginVertical: 10 }, - inputWrapper: { marginBottom: 18 }, - inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 8, marginLeft: 5, letterSpacing: 1 }, - inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 16, borderWidth: 1.5 }, + empresaNome: { fontSize: 17, fontWeight: '800', letterSpacing: -0.3 }, + tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 4 }, + tutorText: { fontSize: 12, fontWeight: '600' }, + arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(26, 54, 93, 0.8)', justifyContent: 'flex-end' }, + modalContent: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 28, maxHeight: '90%' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 30 }, + modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 }, + modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 }, + closeBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' }, + divider: { height: 1, marginVertical: 20, opacity: 0.5 }, + inputWrapper: { marginBottom: 20 }, + inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 58, borderRadius: 18, borderWidth: 1.5 }, textInput: { flex: 1, fontSize: 15, fontWeight: '700' }, - saveBtn: { height: 58, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginTop: 20, elevation: 4 }, - saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 } + saveBtn: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', marginTop: 25, elevation: 6, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 }, + saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2 } }); export default ListaEmpresasProfessor; \ No newline at end of file diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index fb7a2e5..23be623 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -37,7 +37,6 @@ 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), []); @@ -50,24 +49,25 @@ export default function PerfilProfessor() { ]).start(() => setAlertConfig(null)); }, []); - const azulPetroleo = '#2390a6'; + // Cores EPVC + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)', - vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FFF5F5', vermelho: '#EF4444', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#10B981', + verde: '#38A169', }), [isDarkMode]); - useEffect(() => { - carregarPerfil(); - }, []); + useEffect(() => { carregarPerfil(); }, []); async function carregarPerfil() { try { @@ -105,9 +105,9 @@ export default function PerfilProfessor() { if (error) throw error; setEditando(false); - showAlert('Perfil atualizado com sucesso!', 'success'); + showAlert('Perfil atualizado!', 'success'); } catch (error: any) { - showAlert('Erro ao gravar dados no servidor.', 'error'); + showAlert('Erro ao gravar dados.', 'error'); } }; @@ -128,7 +128,6 @@ export default function PerfilProfessor() { - {/* BANNER DE AVISO */} {alertConfig && ( - {/* HEADER */} - router.back()}> - + router.back()}> + - O Meu Perfil + Perfil editando ? guardarPerfil() : setEditando(true)} > - + - {/* AVATAR SECTION */} - + {perfil?.nome?.charAt(0).toUpperCase()} + {editando && ( + + + + )} {perfil?.nome} - - {perfil?.curso || 'Professor / Tutor'} - + + {perfil?.curso || 'Professor'} + - {/* INFO CARD */} - - setPerfil(prev => prev ? { ...prev, nome: v } : null)} - cores={cores} - /> + + setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} /> - setPerfil(prev => prev ? { ...prev, curso: v } : null)} - cores={cores} - /> + setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} /> - + - setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} - cores={cores} - /> + setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} /> - setPerfil(prev => prev ? { ...prev, telefone: v } : null)} - keyboardType="phone-pad" - cores={cores} - /> + setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} /> - {/* ACTIONS */} - router.push('/Professor/redefenirsenha2')} - > + router.push('/Professor/redefenirsenha2')}> - + - Alterar Palavra-passe + Alterar palavra-passe - + - + Terminar Sessão {editando && ( - { setEditando(false); carregarPerfil(); }} - > - Cancelar Edição + { setEditando(false); carregarPerfil(); }}> + Cancelar Alterações )} @@ -269,21 +231,9 @@ export default function PerfilProfessor() { const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( {label} - - - + + + ); @@ -291,54 +241,31 @@ const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( const styles = StyleSheet.create({ safe: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - alertBar: { - position: 'absolute', - left: 20, - right: 20, - padding: 15, - borderRadius: 15, - flexDirection: 'row', - alignItems: 'center', - zIndex: 9999, - elevation: 10, - shadowColor: '#000', - shadowOpacity: 0.2, - shadowRadius: 5 - }, - alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 }, - topBar: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 20, - paddingVertical: 15 - }, - backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, - editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 }, - topTitle: { fontSize: 18, fontWeight: '800' }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, - profileHeader: { alignItems: 'center', marginVertical: 30 }, - avatarContainer: { padding: 8, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' }, - avatar: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center', elevation: 5 }, - avatarLetter: { color: '#fff', fontSize: 32, fontWeight: '800' }, - userName: { fontSize: 22, fontWeight: '800', marginTop: 15 }, - userRole: { fontSize: 14, fontWeight: '500' }, - card: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 }, - inputWrapper: { marginBottom: 15 }, - inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 }, - inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, height: 52, borderRadius: 16, borderWidth: 1.5 }, - textInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 }, + alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, fontSize: 13 }, + topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + editBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1, elevation: 2 }, + topTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }, + scrollContent: { paddingHorizontal: 24, paddingBottom: 50 }, + profileHeader: { alignItems: 'center', marginVertical: 35 }, + avatarBorder: { padding: 4, borderRadius: 100, borderWidth: 2, position: 'relative' }, + avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 }, + avatarLetter: { color: '#fff', fontSize: 36, fontWeight: '900' }, + editBadge: { position: 'absolute', bottom: 0, right: 0, width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 3, borderColor: '#fff' }, + userName: { fontSize: 24, fontWeight: '900', marginTop: 15, letterSpacing: -0.5 }, + roleBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 }, + userRole: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + card: { borderRadius: 28, padding: 24, marginBottom: 20, borderWidth: 1 }, + inputWrapper: { marginBottom: 18 }, + inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 }, + textInput: { flex: 1, fontSize: 15, fontWeight: '700' }, row: { flexDirection: 'row' }, actionsContainer: { gap: 12 }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - padding: 14, - borderRadius: 20, - elevation: 1 - }, - menuIcon: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - menuText: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '700' }, + menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1 }, + menuIcon: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + menuText: { flex: 1, marginLeft: 15, fontSize: 15, fontWeight: '800' }, cancelBtn: { marginTop: 25, alignItems: 'center' }, - cancelText: { fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' } + cancelText: { fontSize: 14, fontWeight: '800', textDecorationLine: 'underline' } }); \ No newline at end of file diff --git a/app/Professor/ProfessorHome.tsx b/app/Professor/ProfessorHome.tsx index 0d08459..2d4b925 100644 --- a/app/Professor/ProfessorHome.tsx +++ b/app/Professor/ProfessorHome.tsx @@ -12,7 +12,6 @@ import { TouchableOpacity, View } from 'react-native'; -// Importação correta para controlo total de áreas seguras import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../themecontext'; import { supabase } from '../lib/supabase'; @@ -22,19 +21,22 @@ const { width } = Dimensions.get('window'); export default function ProfessorMenu() { const router = useRouter(); const { isDarkMode } = useTheme(); - const insets = useSafeAreaInsets(); // Para ajustes finos se necessário + const insets = useSafeAreaInsets(); const [nome, setNome] = useState(''); const [loading, setLoading] = useState(true); - const azulPetroleo = '#2390a6'; + // Paleta EPVC Extraída da Imagem + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - textoSecundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Fundo branco limpo como o login + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', // Azul escuro para contraste + textoSecundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); @@ -61,64 +63,63 @@ export default function ProfessorMenu() { }, []); return ( - // Aplicamos as edges para garantir que o fundo cobre tudo mas o conteúdo respeita os limites - {/* HEADER */} + {/* HEADER EPVC STYLE */} - Bem-vindo, + Olá, Professor {loading ? ( ) : ( - {nome || 'Professor'} + {nome || 'Docente'} )} router.push('/Professor/PerfilProf')} style={[styles.avatarMini, { backgroundColor: cores.azul }]} > - {nome?.charAt(0).toUpperCase()} + {nome?.charAt(0).toUpperCase() || 'P'} - - - Painel de Gestão de Estágios + + + Escola Profissional de Vila do Conde - {/* GRID */} + {/* GRID DE MENU */} - 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/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} /> - EPVC • Gestão de Estágios + + Estágios+ • EPVC 2026 ); } -// O MenuCard permanece igual, apenas garantindo consistência function MenuCard({ icon, title, subtitle, onPress, cores }: any) { return ( - + {title} {subtitle} - + + + ); } const styles = StyleSheet.create({ - content: { padding: 20 }, - header: { marginBottom: 30 }, + content: { padding: 24 }, + header: { marginBottom: 35 }, headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - welcome: { fontSize: 13, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 }, - name: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5, marginTop: 4 }, - avatarMini: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 }, - avatarTxt: { color: '#fff', fontSize: 20, fontWeight: '800' }, - infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 16, borderWidth: 1 }, - bannerTxt: { fontSize: 12, fontWeight: '700', marginLeft: 10, letterSpacing: 0.3 }, + welcome: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }, + name: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8, marginTop: 2 }, + avatarMini: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 8 }, + avatarTxt: { color: '#fff', fontSize: 22, fontWeight: '900' }, + infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 16, borderWidth: 1, borderStyle: 'dashed' }, + bannerTxt: { fontSize: 13, fontWeight: '700', marginLeft: 10 }, grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, card: { - width: (width - 55) / 2, // Ajuste para melhor margem entre cards - borderRadius: 28, - padding: 18, - marginBottom: 15, + width: (width - 64) / 2, + borderRadius: 24, + padding: 20, + marginBottom: 16, borderWidth: 1, shadowColor: '#000', - shadowOpacity: 0.04, - shadowRadius: 10, - elevation: 2, - position: 'relative', - overflow: 'hidden' + shadowOpacity: 0.03, + shadowRadius: 15, + elevation: 3, }, - iconBox: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginBottom: 15 }, - cardTitle: { fontSize: 15, fontWeight: '800' }, - cardSubtitle: { fontSize: 11, marginTop: 2, fontWeight: '600', opacity: 0.8 }, - arrow: { position: 'absolute', top: 18, right: 15, opacity: 0.3 }, - footer: { marginTop: 20, alignItems: 'center', opacity: 0.5 }, - footerTxt: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 } + iconBox: { width: 48, height: 48, borderRadius: 15, justifyContent: 'center', alignItems: 'center', marginBottom: 16 }, + cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.2 }, + cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600', opacity: 0.7 }, + arrowCircle: { position: 'absolute', top: 20, right: 15, width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' }, + footer: { marginTop: 30, alignItems: 'center' }, + footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 }, + footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 } }); \ No newline at end of file diff --git a/app/Professor/defenicoes2.tsx b/app/Professor/defenicoes2.tsx index be17851..5b513c0 100644 --- a/app/Professor/defenicoes2.tsx +++ b/app/Professor/defenicoes2.tsx @@ -23,7 +23,6 @@ const Definicoes = memo(() => { const { isDarkMode, toggleTheme } = useTheme(); const [notificacoes, setNotificacoes] = useState(true); - // --- SISTEMA DE AVISOS MODERNOS --- const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null); const alertOpacity = useMemo(() => new Animated.Value(0), []); @@ -36,19 +35,22 @@ const Definicoes = memo(() => { ]).start(() => setAlertConfig(null)); }, []); - const azulPetroleo = '#2390a6'; + // Cores EPVC + const azulEPVC = '#2390a6'; + const laranjaEPVC = '#E38E00'; const cores = useMemo(() => ({ - fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9', - card: isDarkMode ? '#1A1A1A' : '#FFFFFF', - texto: isDarkMode ? '#F8FAFC' : '#1E293B', - secundario: isDarkMode ? '#94A3B8' : '#64748B', - azul: azulPetroleo, - azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)', + fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Branco puro do login + card: isDarkMode ? '#161618' : '#F8FAFC', + texto: isDarkMode ? '#F8FAFC' : '#1A365D', + secundario: isDarkMode ? '#94A3B8' : '#718096', + azul: azulEPVC, + laranja: laranjaEPVC, + azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA', vermelho: '#EF4444', - vermelhoSuave: 'rgba(239, 68, 68, 0.1)', + vermelhoSuave: '#FFF5F5', borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', - verde: '#10B981', + verde: '#38A169', }), [isDarkMode]); const handleLogout = async () => { @@ -71,7 +73,6 @@ const Definicoes = memo(() => { - {/* BANNER DE FEEDBACK */} {alertConfig && ( { - {/* HEADER ESTILIZADO */} router.back()} > - + - Definições + + Definições + Sistema & Suporte + - {/* GRUPO: PERSONALIZAÇÃO */} - Personalização + Preferências - + - + - Notificações Push + Notificações { setNotificacoes(v); - showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info"); + showAlert(v ? "Notificações ligadas" : "Notificações desligadas", "info"); }} - trackColor={{ false: '#CBD5E1', true: cores.azul }} + trackColor={{ false: '#CBD5E1', true: cores.laranja }} thumbColor="#FFFFFF" /> - + - Interface Escura + Modo Escuro - {/* GRUPO: CONTACTOS EPVC */} - Escola Profissional + Escola Profissional abrirURL('mailto:epvc@epvc.pt')} @@ -147,15 +148,15 @@ const Definicoes = memo(() => { showBorder /> abrirURL('mailto:secretaria@epvc.pt')} cores={cores} showBorder /> abrirURL('tel:252641805')} @@ -163,28 +164,28 @@ const Definicoes = memo(() => { /> - {/* GRUPO: SEGURANÇA & INFO */} - - + + - + - Versão Estável - v2.6.17 + App Version + 2.6.17 - + - Terminar Sessão - + Terminar Sessão + - Desenvolvido para PAP • 2026 - EPVC + + Estágios+ • Vila do Conde + EPVC @@ -193,7 +194,6 @@ const Definicoes = memo(() => { ); }); -// Componente Auxiliar para Links const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => ( {label} - {subLabel} + {subLabel} + + + - ); const styles = StyleSheet.create({ safe: { flex: 1 }, - alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 8 }, - alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1, fontSize: 14 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 }, - btnVoltar: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, + alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 }, + alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1, fontSize: 13 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 }, + btnVoltar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 }, tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 }, - scrollContent: { paddingHorizontal: 20, paddingBottom: 50 }, - sectionLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 1.2 }, - card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 15, elevation: 2 }, - item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16 }, + tituloSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + scrollContent: { paddingHorizontal: 24, paddingBottom: 60 }, + sectionLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, marginLeft: 4, letterSpacing: 1 }, + card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10, elevation: 2 }, + item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 18 }, iconContainer: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, - itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700' }, - versionBadge: { fontSize: 13, fontWeight: 'bold', backgroundColor: 'rgba(0,0,0,0.05)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, - footer: { alignItems: 'center', marginTop: 40, opacity: 0.6 }, - footerText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 } + itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, + versionBadge: { fontSize: 11, fontWeight: '900', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' }, + footer: { alignItems: 'center', marginTop: 50 }, + footerLine: { width: 30, height: 3, borderRadius: 2, marginBottom: 15 }, + footerText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 } }); export default Definicoes; \ No newline at end of file