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"
},