From 7b6263af90797971d810bffb60ed29a9bd36509f Mon Sep 17 00:00:00 2001 From: Ricardo Gomes <230413@epvc.pt> Date: Wed, 11 Mar 2026 12:44:09 +0000 Subject: [PATCH] =?UTF-8?q?aula=20lp=20-=20atualiza=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.json | 3 +- app/Professor/Alunos/CalendarioPresencas.tsx | 261 +++++++++--- app/Professor/Alunos/Estagios.tsx | 409 ++++++++++++------- app/Professor/Empresas/DetalhesEmpresa.tsx | 283 ++++++++----- app/Professor/Empresas/ListaEmpresas.tsx | 365 +++++++++-------- app/Professor/PerfilProf.tsx | 323 +++++++++------ app/Professor/defenicoes2.tsx | 275 +++++++------ package.json | 4 +- 8 files changed, 1191 insertions(+), 732 deletions(-) diff --git a/app.json b/app.json index b829626..1e1a6ba 100644 --- a/app.json +++ b/app.json @@ -9,7 +9,8 @@ "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.anonymous.estagios-pap" }, "android": { "adaptiveIcon": { diff --git a/app/Professor/Alunos/CalendarioPresencas.tsx b/app/Professor/Alunos/CalendarioPresencas.tsx index 9381354..e1a4e73 100644 --- a/app/Professor/Alunos/CalendarioPresencas.tsx +++ b/app/Professor/Alunos/CalendarioPresencas.tsx @@ -2,15 +2,15 @@ import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useMemo } from 'react'; import { - Linking, - Platform, - SafeAreaView, - ScrollView, - StatusBar, - StyleSheet, - Text, - TouchableOpacity, - View, + Linking, + Platform, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, } from 'react-native'; import { useTheme } from '../../../themecontext'; @@ -18,16 +18,22 @@ interface Presenca { data: string; estado: 'presente' | 'faltou'; localizacao?: { lat: number; lng: number }; + hora?: string; // Adicionei hora para o design ficar mais rico } /* DADOS EXEMPLO */ const presencasData: Presenca[] = [ { data: '2024-01-10', + hora: '09:00', estado: 'presente', localizacao: { lat: 41.55, lng: -8.42 }, }, - { data: '2024-01-11', estado: 'faltou' }, + { + data: '2024-01-11', + hora: '09:15', + estado: 'faltou' + }, ]; export default function CalendarioPresencas() { @@ -37,58 +43,112 @@ export default function CalendarioPresencas() { const cores = useMemo( () => ({ - fundo: isDarkMode ? '#121212' : '#f1f3f5', - card: isDarkMode ? '#1e1e1e' : '#fff', - texto: isDarkMode ? '#fff' : '#212529', - verde: '#198754', - vermelho: '#dc3545', - azul: '#0d6efd', + fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC', + card: isDarkMode ? '#1A1A1A' : '#FFFFFF', + texto: isDarkMode ? '#F8FAFC' : '#1E293B', + secundario: isDarkMode ? '#94A3B8' : '#64748B', + verde: '#10B981', + verdeSuave: 'rgba(16, 185, 129, 0.1)', + vermelho: '#EF4444', + vermelhoSuave: 'rgba(239, 68, 68, 0.1)', + azul: '#3B82F6', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode] ); const abrirMapa = (lat: number, lng: number) => { - const url = - Platform.OS === 'ios' - ? `http://maps.apple.com/?ll=${lat},${lng}` - : `https://www.google.com/maps?q=${lat},${lng}`; + const url = Platform.OS === 'ios' + ? `maps://app?saddr=&daddr=${lat},${lng}` + : `google.navigation:q=${lat},${lng}`; Linking.openURL(url); }; return ( - + - - {/* HEADER */} - - router.back()}> - - - {nome} - + {/* HEADER SUPERIOR */} + + router.back()} style={styles.backBtn}> + + + + {nome} + Histórico de Registos + + + + + + + {/* RESUMO RÁPIDO */} + + + PRESENÇAS + 12 + + + FALTAS + 2 + - {/* CALENDÁRIO SIMPLIFICADO */} - {presencasData.map((p, i) => ( - - - {new Date(p.data).toLocaleDateString('pt-PT')} - + Atividade Recente - {p.estado === 'presente' ? ( - abrirMapa(p.localizacao!.lat, p.localizacao!.lng)}> - Presente (ver localização) - - ) : ( - router.push('/Professor/Alunos/Faltas')}> - - Faltou (ver página faltas) - - - )} - - ))} + {presencasData.map((p, i) => { + const isPresente = p.estado === 'presente'; + + return ( + + {/* LINHA LATERAL (TIMELINE) */} + + + {i !== presencasData.length - 1 && } + + + + + + + {new Date(p.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long' })} + + + Registado às {p.hora || '--:--'} + + + + + {p.estado.toUpperCase()} + + + + + + + + {isPresente ? ( + abrirMapa(p.localizacao!.lat, p.localizacao!.lng)} + > + + Ver Localização + + ) : ( + router.push('/Professor/Alunos/Faltas')} + > + + Justificar Falta + + )} + + + + ); + })} ); @@ -97,19 +157,104 @@ export default function CalendarioPresencas() { const styles = StyleSheet.create({ safe: { flex: 1, - paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0, }, - container: { padding: 20 }, - header: { + headerFixed: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 15, + paddingVertical: 15, + borderBottomWidth: 1, + marginTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0, + }, + backBtn: { + width: 40, + height: 40, + justifyContent: 'center', + alignItems: 'center', + }, + headerInfo: { + alignItems: 'center', + }, + title: { fontSize: 18, fontWeight: '800' }, + subtitle: { fontSize: 12, fontWeight: '500' }, + scrollContainer: { padding: 20 }, + + summaryRow: { + flexDirection: 'row', + gap: 12, + marginBottom: 30, + }, + statBox: { + flex: 1, + padding: 15, + borderRadius: 20, + alignItems: 'center', + elevation: 2, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 10, + }, + statLabel: { fontSize: 9, fontWeight: '800', letterSpacing: 1 }, + statValue: { fontSize: 22, fontWeight: '900', marginTop: 4 }, + + sectionTitle: { fontSize: 16, fontWeight: '800', marginBottom: 20 }, + + timelineItem: { + flexDirection: 'row', + }, + timelineLeft: { + width: 30, + alignItems: 'center', + }, + dot: { + width: 12, + height: 12, + borderRadius: 6, + zIndex: 2, + }, + line: { + width: 2, + flex: 1, + marginTop: -5, + }, + card: { + flex: 1, + borderRadius: 20, + padding: 16, + marginBottom: 20, + borderWidth: 1, + marginLeft: 10, + elevation: 3, + shadowColor: '#000', + shadowOpacity: 0.04, + shadowRadius: 8, + }, + cardHeader: { flexDirection: 'row', justifyContent: 'space-between', + alignItems: 'flex-start', + }, + dateText: { fontSize: 15, fontWeight: '700' }, + hourText: { fontSize: 12, marginTop: 2 }, + statusBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + }, + statusText: { fontSize: 9, fontWeight: '900' }, + cardDivider: { + height: 1, + marginVertical: 12, + }, + cardAction: { + flexDirection: 'row', alignItems: 'center', - marginBottom: 20, }, - title: { fontSize: 20, fontWeight: '700' }, - card: { - padding: 16, - borderRadius: 14, - marginBottom: 12, + actionButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, }, -}); + actionText: { fontSize: 13, fontWeight: '700' }, +}); \ No newline at end of file diff --git a/app/Professor/Alunos/Estagios.tsx b/app/Professor/Alunos/Estagios.tsx index 3cd3371..076029c 100644 --- a/app/Professor/Alunos/Estagios.tsx +++ b/app/Professor/Alunos/Estagios.tsx @@ -3,12 +3,17 @@ import { useRouter } from 'expo-router'; import { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, - Alert, Modal, - Platform, - SafeAreaView, ScrollView, + Alert, + Modal, + ScrollView, StatusBar, - StyleSheet, Text, TextInput, TouchableOpacity, View + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; @@ -20,7 +25,7 @@ interface Horario { hora_fim: string; } -interface Aluno { id: string; nome: string; turma_curso: string; } +interface Aluno { id: string; nome: string; turma_curso: string; ano: number; } interface Empresa { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; } interface Estagio { @@ -30,7 +35,7 @@ interface Estagio { data_inicio: string; data_fim: string; horas_diarias?: string; - alunos: { nome: string; turma_curso: string }; + alunos: { nome: string; turma_curso: string; ano: number }; empresas: { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string }; } @@ -39,14 +44,15 @@ export default function Estagios() { const { isDarkMode } = useTheme(); const cores = useMemo(() => ({ - fundo: isDarkMode ? '#121212' : '#f1f3f5', - card: isDarkMode ? '#1e1e1e' : '#fff', - texto: isDarkMode ? '#fff' : '#212529', - textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - border: isDarkMode ? '#343a40' : '#ced4da', - azul: '#0d6efd', - vermelho: '#dc3545', - inputBg: isDarkMode ? '#2c2c2c' : '#f8f9fa', + 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)', + vermelho: '#EF4444', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); // --- Estados --- @@ -69,7 +75,6 @@ export default function Estagios() { const [searchAluno, setSearchAluno] = useState(''); const [searchEmpresa, setSearchEmpresa] = useState(''); - // --- Cálculo de Horas Diárias --- const totalHorasDiarias = useMemo(() => { let totalMinutos = 0; horarios.forEach(h => { @@ -86,15 +91,14 @@ export default function Estagios() { return m > 0 ? `${h}h${m}m` : `${h}h`; }, [horarios]); - // --- Funções de Dados --- useEffect(() => { fetchDados(); }, []); const fetchDados = async () => { try { setLoading(true); const [resEstagios, resAlunos, resEmpresas] = await Promise.all([ - supabase.from('estagios').select('*, alunos(nome, turma_curso), empresas(*)'), - supabase.from('alunos').select('id, nome, turma_curso').order('nome'), + supabase.from('estagios').select('*, alunos(nome, turma_curso, ano), empresas(*)'), + supabase.from('alunos').select('id, nome, turma_curso, ano').order('nome'), supabase.from('empresas').select('*').order('nome') ]); if (resEstagios.data) setEstagios(resEstagios.data); @@ -120,7 +124,7 @@ export default function Estagios() { }; const eliminarEstagio = (id: string) => { - Alert.alert("Eliminar Estágio", "Deseja remover este estágio e todos os seus horários?", [ + Alert.alert("Eliminar Estágio", "Deseja remover este estágio?", [ { text: "Cancelar", style: "cancel" }, { text: "Eliminar", style: "destructive", onPress: async () => { await supabase.from('estagios').delete().eq('id', id); @@ -173,11 +177,10 @@ export default function Estagios() { fetchDados(); }; - // --- Filtros --- const alunosAgrupados = useMemo(() => { const groups: Record = {}; alunos.filter(a => a.nome.toLowerCase().includes(searchAluno.toLowerCase())).forEach(a => { - const k = a.turma_curso || 'Sem Turma'; + const k = a.turma_curso ? `${a.ano}º ${a.turma_curso}` : 'Sem Turma'; if (!groups[k]) groups[k] = []; groups[k].push(a); }); @@ -194,171 +197,279 @@ export default function Estagios() { return groups; }, [empresas, searchEmpresa]); + // --- NOVA LÓGICA DE AGRUPAMENTO (APENAS ISTO FOI ADICIONADO PARA A VIEW) --- + const estagiosAgrupados = useMemo(() => { + const groups: Record = {}; + + estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => { + const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Sem Turma'; + if (!groups[chave]) groups[chave] = []; + groups[chave].push(e); + }); + + return Object.keys(groups).map(titulo => ({ + titulo, + dados: groups[titulo] + })).sort((a, b) => b.titulo.localeCompare(a.titulo)); + }, [estagios, searchMain]); + if (loading) return ; return ( - - + + - - router.back()}> - Estágios - - + + + router.back()}> + + + Estágios + + + + - - - - {estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).map(e => ( - - { - setEditandoEstagio(e); - setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null); - setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null); - setDataInicio(e.data_inicio || ''); - setDataFim(e.data_fim || ''); - carregarHorarios(e.id); - setPasso(2); - setModalVisible(true); - }}> - {e.alunos?.nome} - - {e.alunos?.turma_curso} - ⏱ {e.horas_diarias || '0h'}/dia - - 🏢 {e.empresas?.nome} - - eliminarEstagio(e.id)}> + + + + - ))} - { - setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); - setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true); - }}> - + Novo Estágio - - + {/* RENDERING AGRUPADO AQUI */} + {estagiosAgrupados.map(grupo => ( + + + + {grupo.titulo} + + - + {grupo.dados.map(e => ( + + { + setEditandoEstagio(e); + setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null); + setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null); + setDataInicio(e.data_inicio || ''); + setDataFim(e.data_fim || ''); + carregarHorarios(e.id); + setPasso(2); + setModalVisible(true); + }}> + {e.alunos?.nome} + + + {e.alunos?.turma_curso} + + ⏱ {e.horas_diarias || '0h'}/dia + + 🏢 {e.empresas?.nome} + + + eliminarEstagio(e.id)} + > + + + + ))} + + ))} + + { + setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null); + setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true); + }} + > + + NOVO ESTÁGIO + + + + + + + + {passo === 1 ? "Seleção de Aluno/Empresa" : "Configuração de Estágio"} + + + + + + {passo === 1 ? ( - - Passo 1: Seleção - Aluno - - {Object.keys(alunosAgrupados).map(t => ( - {t} - {alunosAgrupados[t].map(a => ( - setAlunoSelecionado(a)}> - {a.nome} - - ))} - - ))} - - Empresa - - {Object.keys(empresasAgrupadas).map(c => ( - {c} - {empresasAgrupadas[c].map(emp => ( - setEmpresaSelecionada(emp)}> - {emp.nome} - - ))} - - ))} - - - Sair - { if(alunoSelecionado && empresaSelecionada) setPasso(2); }} style={[styles.btnModal, {backgroundColor: cores.azul}]}>Configurar + + SELECIONE O ALUNO + + + {Object.keys(alunosAgrupados).map(t => ( + + {t} + {alunosAgrupados[t].map(a => ( + setAlunoSelecionado(a)} + > + + {a.nome} + + {alunoSelecionado?.id === a.id && } + + ))} + + ))} + + + + SELECIONE A EMPRESA + + + {Object.keys(empresasAgrupadas).map(c => ( + + {c} + {empresasAgrupadas[c].map(emp => ( + setEmpresaSelecionada(emp)} + > + + {emp.nome} + + {empresaSelecionada?.id === emp.id && } + + ))} + + ))} + ) : ( - - Passo 2: Detalhes - - DURAÇÃO E HORAS - - - + + + DURAÇÃO + + + INÍCIO + + + + FIM + + + + + Total: {totalHorasDiarias} - Total Diário: {totalHorasDiarias} - - - HORÁRIOS - setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}> + + + HORÁRIOS + setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}> + + {horarios.map((h, i) => ( { const n = [...horarios]; n[i].periodo = t; setHorarios(n); }} /> { const n = [...horarios]; n[i].hora_inicio = t; setHorarios(n); }} /> - às + ATÉ { const n = [...horarios]; n[i].hora_fim = t; setHorarios(n); }} /> - setHorarios(horarios.filter((_, idx) => idx !== i))}> + setHorarios(horarios.filter((_, idx) => idx !== i))}> + + ))} - - TUTOR - setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome"/> - setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/> - - - - editandoEstagio ? handleFecharModal() : setPasso(1)} - style={[styles.btnModal, {backgroundColor: cores.textoSecundario}]} - > - {editandoEstagio ? 'Cancelar' : 'Voltar'} - - Gravar + + DADOS DO TUTOR + setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome do Tutor"/> + setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/> )} + + + {passo === 2 && !editandoEstagio && ( + setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.azulSuave }]}> + VOLTAR + + )} + { + if(passo === 1) { + if(alunoSelecionado && empresaSelecionada) setPasso(2); + else Alert.alert("Atenção", "Seleciona um aluno e uma empresa!"); + } else { + salvarEstagio(); + } + }} + style={[styles.btnModalPri, { backgroundColor: cores.azul }]} + > + + {passo === 1 ? "PRÓXIMO" : "GRAVAR"} + + + - + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 - }, - header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 }, - title: { fontSize: 22, fontWeight: 'bold' }, - input: { borderWidth: 1, borderRadius: 12, padding: 12, marginBottom: 15 }, - card: { padding: 16, borderRadius: 15, flexDirection: 'row', alignItems: 'center', marginBottom: 10, elevation: 2 }, - cardTitle: { fontSize: 16, fontWeight: 'bold' }, - btnNovo: { padding: 18, borderRadius: 12, alignItems: 'center', marginTop: 10 }, - modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 20 }, - modalContent: { borderRadius: 20, padding: 20, maxHeight: '90%' }, - modalTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 20, textAlign: 'center' }, - label: { fontSize: 14, fontWeight: 'bold', marginBottom: 5 }, - selector: { borderWidth: 1, borderColor: '#eee', borderRadius: 10, overflow: 'hidden' }, - groupHead: { fontSize: 11, backgroundColor: 'rgba(0,0,0,0.05)', padding: 6, fontWeight: 'bold', color: '#666' }, - item: { padding: 12, borderBottomWidth: 0.5, borderColor: '#eee' }, - modalFooter: { flexDirection: 'row', gap: 10, marginTop: 25 }, - btnModal: { flex: 1, padding: 15, borderRadius: 12, alignItems: 'center' }, - confirmBox: { borderWidth: 1, borderRadius: 12, padding: 15, backgroundColor: 'rgba(0,0,0,0.02)' }, - confirmTitle: { fontSize: 10, fontWeight: '900', color: '#0d6efd', marginBottom: 8, letterSpacing: 1 }, - editInput: { borderBottomWidth: 1, paddingVertical: 5, fontSize: 14, borderColor: '#eee' }, - horarioRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 10 }, - miniInput: { borderBottomWidth: 1, borderColor: '#eee', flex: 1, padding: 4, fontSize: 12, textAlign: 'center' }, - badgeHoras: { backgroundColor: '#0d6efd', padding: 5, borderRadius: 6, alignSelf: 'flex-start' }, - badgeText: { color: '#fff', fontSize: 11, fontWeight: 'bold' } + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + btnCircle: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 }, + tituloGeral: { fontSize: 18, fontWeight: '800' }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 40, gap: 15 }, + searchContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 15, borderWidth: 1, marginBottom: 5 }, + searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '500' }, + + // ADICIONADO APENAS PARA O CABEÇALHO DO AGRUPAMENTO: + turmaSectionHeader: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 }, + turmaSectionText: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, + + card: { padding: 18, borderRadius: 24, flexDirection: 'row', alignItems: 'center', marginBottom: 2, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10 }, + cardTitle: { fontSize: 15, fontWeight: '700', marginBottom: 4 }, + row: { flexDirection: 'row', alignItems: 'center', gap: 10 }, + cardSub: { fontSize: 11, fontWeight: '600' }, + empresaText: { fontSize: 12, marginTop: 8, fontWeight: '500' }, + badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + badgeText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase' }, + btnDelete: { width: 36, height: 36, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + btnPrincipal: { height: 56, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 10, gap: 10, elevation: 4 }, + btnPrincipalText: { color: '#fff', fontSize: 14, fontWeight: '800' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' }, + modalContent: { borderTopLeftRadius: 32, borderTopRightRadius: 32, padding: 24, height: '85%' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, + modalTitle: { fontSize: 17, fontWeight: '800' }, + sectionLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2 }, + selectorContainer: { borderWidth: 1, borderRadius: 16, overflow: 'hidden', marginTop: 8 }, + groupHead: { fontSize: 10, padding: 8, fontWeight: '800', textTransform: 'uppercase' }, + item: { padding: 15, borderBottomWidth: 0.5, borderColor: 'rgba(0,0,0,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, + confirmBox: { borderWidth: 1, borderRadius: 20, padding: 16 }, + miniLabel: { fontSize: 9, fontWeight: '800', color: '#94A3B8', marginBottom: 2 }, + editInput: { borderBottomWidth: 1, paddingVertical: 6, fontSize: 14, fontWeight: '600', borderColor: 'rgba(0,0,0,0.1)' }, + horarioRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 12 }, + miniInput: { borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.1)', flex: 1, padding: 6, fontSize: 13, fontWeight: '700', textAlign: 'center' }, + modalFooter: { flexDirection: 'row', gap: 12, marginTop: 30, paddingBottom: 20 }, + btnModalPri: { flex: 2, height: 54, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + btnModalSec: { flex: 1, height: 54, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, }); \ No newline at end of file diff --git a/app/Professor/Empresas/DetalhesEmpresa.tsx b/app/Professor/Empresas/DetalhesEmpresa.tsx index e2ffd1f..26bb365 100644 --- a/app/Professor/Empresas/DetalhesEmpresa.tsx +++ b/app/Professor/Empresas/DetalhesEmpresa.tsx @@ -1,31 +1,43 @@ import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { memo, useMemo, useState } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, - Platform, - SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, + TextInputProps, TouchableOpacity, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; -// Interface atualizada para incluir a lista de alunos export interface Empresa { - id: number; + id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string; - alunos?: string[]; // Array com nomes dos alunos +} + +interface AlunoVinculado { + id: string; + nome: string; +} + +interface InfoItemProps extends TextInputProps { + label: string; + value: string; + icon: keyof typeof Ionicons.glyphMap; + editable: boolean; + onChangeText?: (v: string) => void; + cores: any; } const DetalhesEmpresa = memo(() => { @@ -33,7 +45,6 @@ const DetalhesEmpresa = memo(() => { const router = useRouter(); const params = useLocalSearchParams(); - // Parse seguro dos dados vindos da navegação const empresaOriginal: Empresa = useMemo(() => { if (!params.empresa) return {} as Empresa; const str = Array.isArray(params.empresa) ? params.empresa[0] : params.empresa; @@ -45,18 +56,62 @@ const DetalhesEmpresa = memo(() => { }, [params.empresa]); const [empresaLocal, setEmpresaLocal] = useState({ ...empresaOriginal }); + const [alunos, setAlunos] = useState([]); + const [loadingAlunos, setLoadingAlunos] = useState(true); const [editando, setEditando] = useState(false); const [loading, setLoading] = useState(false); - const cores = { - fundo: isDarkMode ? '#121212' : '#f1f3f5', - card: isDarkMode ? '#1e1e1e' : '#fff', - texto: isDarkMode ? '#fff' : '#000', - textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - azul: '#0d6efd', - verde: '#198754', - vermelho: '#dc3545', - }; + 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)', + vermelho: '#EF4444', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + }), [isDarkMode]); + + useEffect(() => { + if (empresaLocal.id) { + carregarAlunos(); + } + }, [empresaLocal.id]); + + async function carregarAlunos() { + try { + setLoadingAlunos(true); + + const { data: estagios, error: errEstagios } = await supabase + .from('estagios') + .select('aluno_id') + .eq('empresa_id', empresaLocal.id); + + if (errEstagios) throw errEstagios; + + if (!estagios || estagios.length === 0) { + setAlunos([]); + return; + } + + const ids = estagios.map(e => e.aluno_id).filter(id => id !== null); + + const { data: listaAlunos, error: errAlunos } = await supabase + .from('alunos') + .select('id, nome') + .in('id', ids); + + if (errAlunos) throw errAlunos; + + setAlunos(listaAlunos || []); + } catch (error: any) { + console.error("Erro ao carregar lista:", error.message); + setAlunos([]); + } finally { + setLoadingAlunos(false); + } + } const handleSave = async () => { try { @@ -76,14 +131,14 @@ const DetalhesEmpresa = memo(() => { setEditando(false); Alert.alert('Sucesso', 'Dados atualizados!'); } catch (error: any) { - Alert.alert('Erro', error.message); + Alert.alert('Erro', 'Falha ao guardar.'); } finally { setLoading(false); } }; const handleDelete = () => { - Alert.alert('Apagar', `Apagar ${empresaLocal.nome}?`, [ + Alert.alert('Apagar Entidade', `Confirmas a remoção da ${empresaLocal.nome}?`, [ { text: 'Cancelar', style: 'cancel' }, { text: 'Apagar', @@ -98,110 +153,140 @@ const DetalhesEmpresa = memo(() => { return ( - + - + - router.back()}> - + router.back()}> + - {empresaLocal.nome || 'Detalhes'} + Detalhes da Entidade - - setEditando(!editando)} - > - - - - - - + setEditando(!editando)} + > + + - - {/* CARD DE DADOS */} + + - Informações da Empresa - {[ - { label: 'Nome', key: 'nome' }, - { label: 'Curso', key: 'curso' }, - { label: 'Morada', key: 'morada' }, - { label: 'Tutor', key: 'tutor_nome' }, - { label: 'Telefone', key: 'tutor_telefone' }, - ].map((item) => ( - - {item.label} - {editando ? ( - setEmpresaLocal(prev => ({ ...prev, [item.key]: v }))} - /> - ) : ( - {(empresaLocal as any)[item.key] || '---'} - )} - - ))} + Informação da Empresa + setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} /> + setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} /> + setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} /> - {editando && ( - - {loading ? : Confirmar Alterações} - - )} - - {/* ESTATÍSTICAS COM LISTA DE ALUNOS */} - Alunos em Estágio - - {empresaLocal.alunos && empresaLocal.alunos.length > 0 ? ( - empresaLocal.alunos.map((aluno, index) => ( - - - {aluno} + Contacto do Tutor + setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} /> + setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" /> + + + + + Alunos em Estágio + + + {loadingAlunos ? '...' : alunos.length} + + + + + {loadingAlunos ? ( + + ) : alunos.length > 0 ? ( + alunos.map((aluno, index) => ( + + + {aluno.nome.charAt(0).toUpperCase()} + + {aluno.nome} )) ) : ( - - Nenhum aluno associado. - + + + Nenhum aluno associado + )} - - - Total de Alunos - - {empresaLocal.alunos?.length || 0} - - + + {editando ? ( + + {loading ? : Guardar Alterações} + + ) : ( + + + Remover Entidade + + )} + ); }); -export default DetalhesEmpresa; +const InfoItem = ({ label, value, icon, editable, onChangeText, cores, ...props }: InfoItemProps) => ( + + + + + + {label} + {editable ? ( + + ) : ( + {value || '---'} + )} + + +); const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 0 }, - header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 15, paddingVertical: 10 }, - headerAcoes: { flexDirection: 'row', gap: 8 }, - btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2 }, - btnAcao: { width: 38, height: 38, borderRadius: 10, justifyContent: 'center', alignItems: 'center', elevation: 2 }, - tituloGeral: { fontSize: 18, fontWeight: 'bold', flex: 1, textAlign: 'center', marginHorizontal: 10 }, - container: { padding: 20, gap: 15 }, - card: { padding: 20, borderRadius: 16, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 }, - tituloCard: { fontSize: 16, fontWeight: 'bold', marginBottom: 15, textAlign: 'center', borderBottomWidth: 1, borderBottomColor: '#f0f0f0', paddingBottom: 8 }, - campoWrapper: { marginBottom: 15 }, - label: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 }, - valor: { fontSize: 16, fontWeight: '500' }, - alunoRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, paddingLeft: 5 }, - input: { borderWidth: 1, borderRadius: 8, padding: 10, fontSize: 16, marginTop: 2 }, - saveButton: { padding: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', marginBottom: 10 }, - txtBtn: { color: '#fff', fontWeight: 'bold', fontSize: 16 } -}); \ No newline at end of file + safe: { flex: 1 }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 }, + btnCircle: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 }, + tituloGeral: { fontSize: 18, fontWeight: '800', flex: 1, textAlign: 'center', marginHorizontal: 15 }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 40, gap: 15 }, + card: { padding: 20, borderRadius: 24, elevation: 2 }, + sectionLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 1 }, + infoWrapper: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 }, + infoIcon: { width: 36, height: 36, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + infoLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase', marginBottom: 2 }, + infoValue: { fontSize: 15, fontWeight: '600' }, + infoInput: { fontSize: 15, fontWeight: '600', borderBottomWidth: 1, paddingVertical: 2 }, + alunosHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }, + badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 }, + badgeText: { fontSize: 12, fontWeight: '800' }, + alunoItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 }, + alunoAvatar: { width: 28, height: 28, borderRadius: 8, justifyContent: 'center', alignItems: 'center', marginRight: 10 }, + alunoNome: { flex: 1, fontSize: 14, fontWeight: '600' }, + emptyAlunos: { alignItems: 'center', paddingVertical: 10 }, + btnPrincipal: { height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center', marginTop: 10 }, + btnPrincipalText: { color: '#fff', fontSize: 16, fontWeight: '800' }, + btnDelete: { height: 56, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 10, gap: 8 }, + btnDeleteText: { fontSize: 16, fontWeight: '700' } +}); + +export default DetalhesEmpresa; \ No newline at end of file diff --git a/app/Professor/Empresas/ListaEmpresas.tsx b/app/Professor/Empresas/ListaEmpresas.tsx index 1856cdb..78b3a97 100644 --- a/app/Professor/Empresas/ListaEmpresas.tsx +++ b/app/Professor/Empresas/ListaEmpresas.tsx @@ -4,22 +4,24 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, - FlatList, + KeyboardAvoidingView, Modal, Platform, RefreshControl, - SafeAreaView, ScrollView, + SectionList, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, - View + View, } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../../themecontext'; import { supabase } from '../../lib/supabase'; +// --- INTERFACES --- export interface Empresa { id: number; nome: string; @@ -32,13 +34,15 @@ export interface Empresa { const ListaEmpresasProfessor = memo(() => { const { isDarkMode } = useTheme(); const router = useRouter(); + const insets = useSafeAreaInsets(); + // --- ESTADOS --- const [search, setSearch] = useState(''); const [empresas, setEmpresas] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - // MODAL + FORM + // Estados do Formulário (Modal) const [modalVisible, setModalVisible] = useState(false); const [nome, setNome] = useState(''); const [morada, setMorada] = useState(''); @@ -46,20 +50,25 @@ const ListaEmpresasProfessor = memo(() => { const [tutorTelefone, setTutorTelefone] = useState(''); const [curso, setCurso] = useState(''); + // --- CORES DINÂMICAS --- const cores = useMemo(() => ({ - fundo: isDarkMode ? '#121212' : '#f1f3f5', - card: isDarkMode ? '#1e1e1e' : '#fff', - texto: isDarkMode ? '#fff' : '#000', - textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - azul: '#0d6efd', + 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', }), [isDarkMode]); + // --- FUNÇÕES --- const fetchEmpresas = async () => { try { setLoading(true); const { data, error } = await supabase .from('empresas') .select('*') + .order('curso', { ascending: true }) .order('nome', { ascending: true }); if (error) throw error; @@ -81,23 +90,36 @@ const ListaEmpresasProfessor = memo(() => { fetchEmpresas(); }, []); - const filteredEmpresas = useMemo( - () => empresas.filter(e => - e.nome?.toLowerCase().includes(search.toLowerCase()) - ), - [search, empresas] - ); + // --- CORREÇÃO DO AGRUPAMENTO --- + const secoesAgrupadas = useMemo(() => { + const filtradas = empresas.filter(e => + e.nome?.toLowerCase().includes(search.toLowerCase()) || + e.curso?.toLowerCase().includes(search.toLowerCase()) + ); + + const grupos = filtradas.reduce((acc: { [key: string]: Empresa[] }, empresa) => { + // Normalizamos o nome do curso para evitar duplicados (Tudo maiúsculas e sem espaços extras) + const cursoKey = (empresa.curso || 'Sem Curso').trim().toUpperCase(); + + if (!acc[cursoKey]) acc[cursoKey] = []; + acc[cursoKey].push(empresa); + return acc; + }, {}); + + return Object.keys(grupos).map(cursoNome => ({ + title: cursoNome, + data: grupos[cursoNome], + })).sort((a, b) => a.title.localeCompare(b.title)); + }, [search, empresas]); - // 👉 CRIAR EMPRESA (AGORA COM TODOS OS CAMPOS) const criarEmpresa = async () => { if (!nome || !morada || !tutorNome || !tutorTelefone || !curso) { - Alert.alert('Erro', 'Preenche todos os campos.'); + Alert.alert('Atenção', 'Preenche todos os campos para não dar merda.'); return; } try { setLoading(true); - const { data, error } = await supabase .from('empresas') .insert([{ @@ -105,23 +127,17 @@ const ListaEmpresasProfessor = memo(() => { morada: morada.trim(), tutor_nome: tutorNome.trim(), tutor_telefone: tutorTelefone.trim(), - curso: curso.trim(), + curso: curso.trim(), // O trim aqui já ajuda na base de dados }]) .select(); if (error) throw error; - setEmpresas(prev => [data![0], ...prev]); - - // limpar form - setNome(''); - setMorada(''); - setTutorNome(''); - setTutorTelefone(''); - setCurso(''); - + setEmpresas(prev => [...prev, data![0]]); setModalVisible(false); - Alert.alert('Sucesso', 'Empresa criada com sucesso!'); + + setNome(''); setMorada(''); setTutorNome(''); setTutorTelefone(''); setCurso(''); + Alert.alert('Sucesso', 'Nova empresa registada!'); } catch (error: any) { Alert.alert('Erro ao criar', error.message); } finally { @@ -131,60 +147,79 @@ const ListaEmpresasProfessor = memo(() => { return ( - - + + {/* HEADER */} - router.back()} > - + + + + + Empresas + + {empresas.length} entidades no total + + + + setModalVisible(true)} + > + - - Empresas - - - {/* SEARCH */} - - - + {/* SEARCH BAR */} + + + + + - {/* BOTÃO NOVA EMPRESA */} - setModalVisible(true)} - > - - Nova Empresa - - - {/* LISTA */} + {/* CONTEÚDO / LISTA */} {loading && !refreshing ? ( - + + + ) : ( - item.id.toString()} + stickySectionHeadersEnabled={false} + contentContainerStyle={[ + styles.listPadding, + { paddingBottom: insets.bottom + 100 } + ]} refreshControl={ } + renderSectionHeader={({ section: { title } }) => ( + + + {title} + + )} renderItem={({ item }) => ( router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', @@ -192,130 +227,122 @@ const ListaEmpresasProfessor = memo(() => { }) } > - - {item.nome} - - {item.curso} - - - {item.tutor_nome} - + + - + + + {item.nome} + + + {item.tutor_nome} + + + + )} - contentContainerStyle={styles.listContent} + ListEmptyComponent={() => ( + + + Nenhuma empresa encontrada. + + )} /> )} - {/* MODAL */} + {/* MODAL DE CRIAÇÃO */} - - - - Nova Empresa - - - - - - - - - - - + + + + Registar Empresa setModalVisible(false)}> - - Cancelar - - - - - - Criar - + + + + + + + + + + + Guardar Empresa + + - + ); }); -export default ListaEmpresasProfessor; - -/* INPUT */ -const Input = ({ label, cores, ...props }: any) => ( - - - {label} - - +const ModernInput = ({ label, icon, cores, ...props }: any) => ( + + {label} + + + + ); const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? 10 : 0 }, - header: { flexDirection: 'row', justifyContent: 'space-between', padding: 20 }, - btnVoltar: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' }, - tituloGeral: { fontSize: 22, fontWeight: 'bold' }, - searchContainer: { marginHorizontal: 15, marginBottom: 10 }, - searchIcon: { position: 'absolute', left: 15, top: 14 }, - search: { borderRadius: 12, padding: 12, paddingLeft: 45 }, - btnCriar: { flexDirection: 'row', margin: 15, padding: 15, borderRadius: 12, justifyContent: 'center', gap: 8 }, - txtBtnCriar: { color: '#fff', fontWeight: 'bold' }, - listContent: { padding: 15 }, - card: { flexDirection: 'row', justifyContent: 'space-between', padding: 18, borderRadius: 15, marginBottom: 12 }, - nomeEmpresa: { fontSize: 18, fontWeight: 'bold' }, - info: { fontSize: 14, marginTop: 3 }, - - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.5)', - justifyContent: 'center', - padding: 20, - }, - modalContent: { - borderRadius: 16, - padding: 20, - maxHeight: '90%', - }, - modalTitle: { - fontSize: 20, - fontWeight: 'bold', - marginBottom: 15, - textAlign: 'center', - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: 15, - }, - btnConfirmar: { - padding: 14, - borderRadius: 10, - minWidth: 100, - alignItems: 'center', - }, + safe: { flex: 1 }, + header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 15, paddingTop: Platform.OS === 'android' ? 10 : 0 }, + backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + headerTitle: { fontSize: 22, fontWeight: '800' }, + headerSubtitle: { fontSize: 13, fontWeight: '500' }, + addBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 4 }, + searchSection: { paddingHorizontal: 20, marginBottom: 10 }, + searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 15, borderWidth: 1 }, + searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' }, + loadingCenter: { marginTop: 50, alignItems: 'center' }, + emptyContainer: { marginTop: 80, alignItems: 'center', justifyContent: 'center' }, + listPadding: { paddingHorizontal: 20 }, + sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 12, marginLeft: 5 }, + sectionLine: { width: 4, height: 16, borderRadius: 2, marginRight: 8 }, + sectionTitle: { fontSize: 14, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }, + empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 18, marginBottom: 10, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 8, shadowOffset: { width: 0, height: 2 } }, + empresaIcon: { width: 46, height: 46, borderRadius: 14, justifyContent: 'center', alignItems: 'center' }, + empresaInfo: { flex: 1, marginLeft: 12 }, + empresaNome: { fontSize: 15, fontWeight: '700' }, + tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 }, + tutorText: { fontSize: 12, fontWeight: '500' }, + modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' }, + modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 25, maxHeight: '90%' }, + modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, + modalTitle: { fontSize: 20, fontWeight: '800' }, + inputWrapper: { marginBottom: 15 }, + inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 }, + inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, height: 48, borderRadius: 12, borderWidth: 1 }, + textInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + saveBtn: { height: 52, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginTop: 15 }, + saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' } }); + +export default ListaEmpresasProfessor; \ No newline at end of file diff --git a/app/Professor/PerfilProf.tsx b/app/Professor/PerfilProf.tsx index 8896212..a1247f6 100644 --- a/app/Professor/PerfilProf.tsx +++ b/app/Professor/PerfilProf.tsx @@ -1,11 +1,9 @@ import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, - Platform, - SafeAreaView, ScrollView, StatusBar, StyleSheet, @@ -14,6 +12,7 @@ import { TouchableOpacity, View } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../themecontext'; import { supabase } from '../lib/supabase'; @@ -25,27 +24,30 @@ interface PerfilData { telefone: string; residencia: string; tipo: string; - curso: string; // Sincronizado com a coluna que criaste no Supabase + curso: string; idade?: number; } export default function PerfilProfessor() { const router = useRouter(); const { isDarkMode } = useTheme(); + const insets = useSafeAreaInsets(); + const [editando, setEditando] = useState(false); const [loading, setLoading] = useState(true); const [perfil, setPerfil] = useState(null); - const cores = { - fundo: isDarkMode ? '#121212' : '#f1f3f5', - card: isDarkMode ? '#1e1e1e' : '#fff', - texto: isDarkMode ? '#fff' : '#212529', - textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d', - inputBg: isDarkMode ? '#2c2c2c' : '#f8f9fa', - border: isDarkMode ? '#343a40' : '#ced4da', - azul: '#0d6efd', - vermelho: '#dc3545', - }; + 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)', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + vermelho: '#EF4444', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', + }), [isDarkMode]); useEffect(() => { carregarPerfil(); @@ -65,7 +67,7 @@ export default function PerfilProfessor() { setPerfil(data); } } catch (error: any) { - Alert.alert('Erro', 'Não foi possível carregar os dados do perfil.'); + Alert.alert('Erro', 'Não foi possível carregar os dados.'); } finally { setLoading(false); } @@ -81,16 +83,15 @@ export default function PerfilProfessor() { telefone: perfil.telefone, residencia: perfil.residencia, n_escola: perfil.n_escola, - curso: perfil.curso // Usando o nome correto da coluna + curso: perfil.curso }) .eq('id', perfil.id); if (error) throw error; setEditando(false); - Alert.alert('Sucesso', 'Perfil atualizado com sucesso!'); + Alert.alert('Sucesso', 'Perfil atualizado!'); } catch (error: any) { - // Se der erro aqui, vai dar merda porque o nome da coluna pode estar mal escrito no Supabase - Alert.alert('Erro ao gravar', 'Verifica se a coluna se chama exatamente "curso". ' + error.message); + Alert.alert('Erro ao gravar', 'Verifica a coluna "curso" no Supabase para não dar merda.'); } }; @@ -108,140 +109,190 @@ export default function PerfilProfessor() { } return ( - - + + - + + + {/* HEADER */} - router.back()}> - + router.back()}> + O Meu Perfil - + editando ? guardarPerfil() : setEditando(true)} + > + + - - - - - {perfil?.nome} - - {perfil?.curso || 'Sem curso definido'} - - - - - setPerfil(prev => prev ? { ...prev, nome: v } : null)} - cores={cores} - /> - - setPerfil(prev => prev ? { ...prev, curso: v } : null)} - cores={cores} - /> - - + - setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} - cores={cores} - /> + {/* AVATAR SECTION */} + + + + {perfil?.nome?.charAt(0).toUpperCase()} + + + {perfil?.nome} + + {perfil?.curso || 'Professor'} + + - setPerfil(prev => prev ? { ...prev, telefone: v } : null)} - cores={cores} - /> - + {/* INFO CARD */} + + setPerfil(prev => prev ? { ...prev, nome: v } : null)} + cores={cores} + /> - - {editando ? ( - - - Guardar Alterações + setPerfil(prev => prev ? { ...prev, curso: 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} + /> + + + + + {/* ACTIONS */} + + router.push('/Professor/redefenirsenha2')} + > + + + + Alterar Palavra-passe + - ) : ( - setEditando(true)}> - - Editar Perfil + + + + + + Terminar Sessão + + + {editando && ( + { setEditando(false); carregarPerfil(); }} + > + Cancelar Edição + )} - router.push('/Professor/redefenirsenha2')} - > - - Alterar Palavra-passe - - - - - Terminar Sessão - - - - - ); -} - -function InfoField({ label, value, editable, onChange, cores }: any) { - return ( - - {label} - {editable ? ( - - ) : ( - {value || 'Não definido'} - )} + + ); } +const ModernInput = ({ label, icon, cores, editable, ...props }: any) => ( + + {label} + + + + + +); + const styles = StyleSheet.create({ - safeArea: { - flex: 1, - paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 - }, + safe: { flex: 1 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - content: { padding: 24, paddingBottom: 40 }, - topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, marginTop: 10 }, - topTitle: { fontSize: 18, fontWeight: '700' }, - header: { alignItems: 'center', marginBottom: 32 }, - avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', marginBottom: 12 }, - name: { fontSize: 22, fontWeight: '800' }, - role: { fontSize: 14, marginTop: 4 }, - card: { borderRadius: 18, padding: 20, marginBottom: 24, elevation: 4 }, - infoField: { marginBottom: 16 }, - label: { fontSize: 11, marginBottom: 4, textTransform: 'uppercase', fontWeight: '600' }, - value: { fontSize: 16, fontWeight: '600' }, - input: { borderWidth: 1, borderRadius: 10, padding: 12, fontSize: 15 }, - actions: { gap: 12 }, - actionButton: { flexDirection: 'row', alignItems: 'center', borderRadius: 14, padding: 16 }, - actionText: { fontSize: 15, fontWeight: '600', marginLeft: 12 }, - primaryButton: { borderRadius: 14, padding: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }, - primaryText: { color: '#fff', fontWeight: '700', marginLeft: 8 }, + 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: 48, borderRadius: 14, borderWidth: 1 }, + textInput: { flex: 1, fontSize: 15, fontWeight: '600' }, + row: { flexDirection: 'row' }, + actionsContainer: { gap: 10 }, + menuItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + borderRadius: 18, + elevation: 1 + }, + menuIcon: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' }, + menuText: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '700' }, + cancelBtn: { marginTop: 20, alignItems: 'center' }, + cancelText: { fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' } }); \ No newline at end of file diff --git a/app/Professor/defenicoes2.tsx b/app/Professor/defenicoes2.tsx index c2b1817..25bdaaf 100644 --- a/app/Professor/defenicoes2.tsx +++ b/app/Professor/defenicoes2.tsx @@ -1,34 +1,37 @@ import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; -import { memo, useMemo, useState } from 'react'; // Importado useMemo e memo +import { memo, useMemo, useState } from 'react'; import { - Alert, - Linking, - Platform, - SafeAreaView, - StatusBar, - StyleSheet, - Switch, - Text, - TouchableOpacity, - View + Alert, + Linking, + Platform, + ScrollView, + StatusBar, + StyleSheet, + Switch, + Text, + TouchableOpacity, + View } from 'react-native'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../themecontext'; const Definicoes = memo(() => { const router = useRouter(); + const insets = useSafeAreaInsets(); const [notificacoes, setNotificacoes] = useState(true); const { isDarkMode, toggleTheme } = useTheme(); - // 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' : '#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)', + vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)', + vermelho: '#EF4444', + borda: isDarkMode ? '#2D2D2D' : '#E2E8F0', }), [isDarkMode]); const handleLogout = () => { @@ -46,116 +49,152 @@ const Definicoes = memo(() => { ); }; - 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.openURL(url); 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 - - + + + {/* HEADER */} + router.back()} > - - - Terminar Sessão - - + - + Definições + - - + + + + {/* SECÇÃO PREFERÊNCIAS */} + Preferências + + + + + + Notificações + + + + + + + + Modo Escuro + + + + + {/* SECÇÃO SUPORTE */} + Suporte e Contactos + + abrirURL('mailto:epvc@epvc.pt')} + > + + + + + Direção + epvc@epvc.pt + + + + + abrirURL('mailto:secretaria@epvc.pt')} + > + + + + + Secretaria + secretaria@epvc.pt + + + + + abrirURL('tel:252641805')} + > + + + + + Telefone + 252 641 805 + + + + + + {/* SECÇÃO INFO & SAIR */} + + + + + + Versão da App + 26.3.11 + + + + + + + Terminar Sessão + + + + + Escola Profissional de Vila do Conde © 2026 + + + + ); }); const styles = StyleSheet.create({ - safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }, - 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' } + safe: { flex: 1 }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingVertical: 15 + }, + btnVoltar: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4 }, + tituloGeral: { fontSize: 20, fontWeight: '800' }, + scrollContent: { paddingHorizontal: 20, paddingBottom: 40 }, + sectionTitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 10, marginLeft: 5, letterSpacing: 1 }, + card: { borderRadius: 24, paddingHorizontal: 16, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } }, + item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, borderBottomWidth: 1 }, + iconBox: { width: 38, height: 38, borderRadius: 10, justifyContent: 'center', alignItems: 'center' }, + itemTexto: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' }, + footerText: { textAlign: 'center', marginTop: 30, fontSize: 12, fontWeight: '600', opacity: 0.5 } }); export default Definicoes; \ No newline at end of file diff --git a/package.json b/package.json index 621ca15..c075539 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", - "android": "expo start --android", - "ios": "expo start --ios", + "android": "expo run:android", + "ios": "expo run:ios", "web": "expo start --web", "lint": "expo lint" },