aula lp - atualização

This commit is contained in:
2026-03-11 12:44:09 +00:00
parent f468c926e7
commit 7b6263af90
8 changed files with 1191 additions and 732 deletions

View File

@@ -9,7 +9,8 @@
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
"supportsTablet": true,
"bundleIdentifier": "com.anonymous.estagios-pap"
},
"android": {
"adaptiveIcon": {

View File

@@ -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 (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<ScrollView contentContainerStyle={styles.container}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="arrow-back" size={26} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>{nome}</Text>
<View style={{ width: 26 }} />
{/* HEADER SUPERIOR */}
<View style={[styles.headerFixed, { borderBottomColor: cores.borda }]}>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Ionicons name="chevron-back" size={24} color={cores.texto} />
</TouchableOpacity>
<View style={styles.headerInfo}>
<Text style={[styles.title, { color: cores.texto }]}>{nome}</Text>
<Text style={[styles.subtitle, { color: cores.secundario }]}>Histórico de Registos</Text>
</View>
<View style={{ width: 40 }} />
</View>
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
{/* RESUMO RÁPIDO */}
<View style={styles.summaryRow}>
<View style={[styles.statBox, { backgroundColor: cores.card }]}>
<Text style={[styles.statLabel, { color: cores.secundario }]}>PRESENÇAS</Text>
<Text style={[styles.statValue, { color: cores.verde }]}>12</Text>
</View>
<View style={[styles.statBox, { backgroundColor: cores.card }]}>
<Text style={[styles.statLabel, { color: cores.secundario }]}>FALTAS</Text>
<Text style={[styles.statValue, { color: cores.vermelho }]}>2</Text>
</View>
</View>
{/* CALENDÁRIO SIMPLIFICADO */}
{presencasData.map((p, i) => (
<View key={i} style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={{ color: cores.texto, fontWeight: '600' }}>
{new Date(p.data).toLocaleDateString('pt-PT')}
</Text>
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Atividade Recente</Text>
{p.estado === 'presente' ? (
<TouchableOpacity onPress={() => abrirMapa(p.localizacao!.lat, p.localizacao!.lng)}>
<Text style={{ color: cores.verde }}>Presente (ver localização)</Text>
</TouchableOpacity>
) : (
<TouchableOpacity onPress={() => router.push('/Professor/Alunos/Faltas')}>
<Text style={{ color: cores.vermelho }}>
Faltou (ver página faltas)
</Text>
</TouchableOpacity>
)}
</View>
))}
{presencasData.map((p, i) => {
const isPresente = p.estado === 'presente';
return (
<View key={i} style={styles.timelineItem}>
{/* LINHA LATERAL (TIMELINE) */}
<View style={styles.timelineLeft}>
<View style={[styles.dot, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
{i !== presencasData.length - 1 && <View style={[styles.line, { backgroundColor: cores.borda }]} />}
</View>
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={styles.cardHeader}>
<View>
<Text style={[styles.dateText, { color: cores.texto }]}>
{new Date(p.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long' })}
</Text>
<Text style={[styles.hourText, { color: cores.secundario }]}>
Registado às {p.hora || '--:--'}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: isPresente ? cores.verdeSuave : cores.vermelhoSuave }]}>
<Text style={[styles.statusText, { color: isPresente ? cores.verde : cores.vermelho }]}>
{p.estado.toUpperCase()}
</Text>
</View>
</View>
<View style={[styles.cardDivider, { backgroundColor: cores.borda }]} />
<View style={styles.cardAction}>
{isPresente ? (
<TouchableOpacity
style={styles.actionButton}
onPress={() => abrirMapa(p.localizacao!.lat, p.localizacao!.lng)}
>
<Ionicons name="location-outline" size={18} color={cores.azul} />
<Text style={[styles.actionText, { color: cores.azul }]}>Ver Localização</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.actionButton}
onPress={() => router.push('/Professor/Alunos/Faltas')}
>
<Ionicons name="alert-circle-outline" size={18} color={cores.vermelho} />
<Text style={[styles.actionText, { color: cores.vermelho }]}>Justificar Falta</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
);
})}
</ScrollView>
</SafeAreaView>
);
@@ -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' },
});

View File

@@ -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<string, Aluno[]> = {};
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<string, Estagio[]> = {};
estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => {
const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Sem Turma';
if (!groups[chave]) groups[chave] = [];
groups[chave].push(e);
});
return Object.keys(groups).map(titulo => ({
titulo,
dados: groups[titulo]
})).sort((a, b) => b.titulo.localeCompare(a.titulo));
}, [estagios, searchMain]);
if (loading) return <View style={{flex:1, justifyContent:'center', backgroundColor:cores.fundo}}><ActivityIndicator size="large" color={cores.azul}/></View>;
return (
<SafeAreaView style={[styles.container, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} translucent backgroundColor="transparent" />
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}><Ionicons name="arrow-back" size={26} color={cores.texto}/></TouchableOpacity>
<Text style={[styles.title, { color: cores.texto }]}>Estágios</Text>
<TouchableOpacity onPress={fetchDados}><Ionicons name="refresh" size={24} color={cores.azul}/></TouchableOpacity>
</View>
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<View style={styles.header}>
<TouchableOpacity style={[styles.btnCircle, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto}/>
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Estágios</Text>
<TouchableOpacity style={[styles.btnCircle, { backgroundColor: cores.card }]} onPress={fetchDados}>
<Ionicons name="refresh" size={20} color={cores.azul}/>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={{ padding: 20 }}>
<TextInput
placeholder="Procurar aluno..."
placeholderTextColor={cores.textoSecundario}
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.border }]}
onChangeText={setSearchMain}
/>
{estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).map(e => (
<View key={e.id} style={[styles.card, { backgroundColor: cores.card }]}>
<TouchableOpacity style={{ flex: 1 }} onPress={() => {
setEditandoEstagio(e);
setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null);
setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null);
setDataInicio(e.data_inicio || '');
setDataFim(e.data_fim || '');
carregarHorarios(e.id);
setPasso(2);
setModalVisible(true);
}}>
<Text style={[styles.cardTitle, { color: cores.texto }]}>{e.alunos?.nome}</Text>
<View style={{flexDirection: 'row', gap: 10, marginTop: 4}}>
<Text style={{ color: cores.azul, fontSize: 11, fontWeight: '700' }}>{e.alunos?.turma_curso}</Text>
<Text style={{ color: cores.textoSecundario, fontSize: 11 }}> {e.horas_diarias || '0h'}/dia</Text>
</View>
<Text style={{ color: cores.textoSecundario, marginTop: 4, fontSize: 13 }}>🏢 {e.empresas?.nome}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => eliminarEstagio(e.id)}><Ionicons name="trash-outline" size={20} color={cores.vermelho} /></TouchableOpacity>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<View style={[styles.searchContainer, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search" size={18} color={cores.secundario} />
<TextInput
placeholder="Procurar estagiário..."
placeholderTextColor={cores.secundario}
style={[styles.searchInput, { color: cores.texto }]}
onChangeText={setSearchMain}
/>
</View>
))}
<TouchableOpacity style={[styles.btnNovo, { backgroundColor: cores.azul }]} onPress={() => {
setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true);
}}>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>+ Novo Estágio</Text>
</TouchableOpacity>
</ScrollView>
{/* RENDERING AGRUPADO AQUI */}
{estagiosAgrupados.map(grupo => (
<View key={grupo.titulo} style={{ marginBottom: 20 }}>
<View style={[styles.turmaSectionHeader, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.turmaSectionText, { color: cores.azul }]}>
{grupo.titulo}
</Text>
</View>
<Modal visible={modalVisible} animationType="slide" transparent>
{grupo.dados.map(e => (
<View key={e.id} style={[styles.card, { backgroundColor: cores.card }]}>
<TouchableOpacity style={{ flex: 1 }} onPress={() => {
setEditandoEstagio(e);
setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null);
setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null);
setDataInicio(e.data_inicio || '');
setDataFim(e.data_fim || '');
carregarHorarios(e.id);
setPasso(2);
setModalVisible(true);
}}>
<Text style={[styles.cardTitle, { color: cores.texto }]}>{e.alunos?.nome}</Text>
<View style={styles.row}>
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.badgeText, { color: cores.azul }]}>{e.alunos?.turma_curso}</Text>
</View>
<Text style={[styles.cardSub, { color: cores.secundario }]}> {e.horas_diarias || '0h'}/dia</Text>
</View>
<Text style={[styles.empresaText, { color: cores.secundario }]}>🏢 {e.empresas?.nome}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btnDelete, { backgroundColor: cores.vermelhoSuave }]}
onPress={() => eliminarEstagio(e.id)}
>
<Ionicons name="trash-outline" size={18} color={cores.vermelho} />
</TouchableOpacity>
</View>
))}
</View>
))}
<TouchableOpacity
style={[styles.btnPrincipal, { backgroundColor: cores.azul }]}
onPress={() => {
setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHorarios([]); setPasso(1); setModalVisible(true);
}}
>
<Ionicons name="add" size={22} color="#fff" />
<Text style={styles.btnPrincipalText}>NOVO ESTÁGIO</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
<Modal visible={modalVisible} animationType="fade" transparent>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>
{passo === 1 ? "Seleção de Aluno/Empresa" : "Configuração de Estágio"}
</Text>
<TouchableOpacity onPress={handleFecharModal}>
<Ionicons name="close-circle" size={28} color={cores.secundario} />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{passo === 1 ? (
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Passo 1: Seleção</Text>
<Text style={styles.label}>Aluno</Text>
<View style={styles.selector}><ScrollView nestedScrollEnabled style={{maxHeight: 180}}>
{Object.keys(alunosAgrupados).map(t => (
<View key={t}><Text style={styles.groupHead}>{t}</Text>
{alunosAgrupados[t].map(a => (
<TouchableOpacity key={a.id} style={[styles.item, alunoSelecionado?.id === a.id && {backgroundColor: cores.azul}]} onPress={() => setAlunoSelecionado(a)}>
<Text style={{color: alunoSelecionado?.id === a.id ? '#fff' : cores.texto}}>{a.nome}</Text>
</TouchableOpacity>
))}
</View>
))}
</ScrollView></View>
<Text style={[styles.label, {marginTop: 15}]}>Empresa</Text>
<View style={styles.selector}><ScrollView nestedScrollEnabled style={{maxHeight: 180}}>
{Object.keys(empresasAgrupadas).map(c => (
<View key={c}><Text style={styles.groupHead}>{c}</Text>
{empresasAgrupadas[c].map(emp => (
<TouchableOpacity key={emp.id} style={[styles.item, empresaSelecionada?.id === emp.id && {backgroundColor: cores.azul}]} onPress={() => setEmpresaSelecionada(emp)}>
<Text style={{color: empresaSelecionada?.id === emp.id ? '#fff' : cores.texto}}>{emp.nome}</Text>
</TouchableOpacity>
))}
</View>
))}
</ScrollView></View>
<View style={styles.modalFooter}>
<TouchableOpacity onPress={handleFecharModal} style={[styles.btnModal, {backgroundColor: cores.vermelho}]}><Text style={{color:'#fff'}}>Sair</Text></TouchableOpacity>
<TouchableOpacity onPress={() => { if(alunoSelecionado && empresaSelecionada) setPasso(2); }} style={[styles.btnModal, {backgroundColor: cores.azul}]}><Text style={{color:'#fff'}}>Configurar</Text></TouchableOpacity>
<View style={{ gap: 15 }}>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>SELECIONE O ALUNO</Text>
<View style={[styles.selectorContainer, { borderColor: cores.borda }]}>
<ScrollView nestedScrollEnabled style={{maxHeight: 200}}>
{Object.keys(alunosAgrupados).map(t => (
<View key={t}>
<Text style={[styles.groupHead, { color: cores.azul, backgroundColor: cores.azulSuave }]}>{t}</Text>
{alunosAgrupados[t].map(a => (
<TouchableOpacity
key={a.id}
style={[styles.item, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]}
onPress={() => setAlunoSelecionado(a)}
>
<Text style={{color: alunoSelecionado?.id === a.id ? cores.azul : cores.texto, fontWeight: alunoSelecionado?.id === a.id ? 'bold' : '400'}}>
{a.nome}
</Text>
{alunoSelecionado?.id === a.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
</TouchableOpacity>
))}
</View>
))}
</ScrollView>
</View>
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 10 }]}>SELECIONE A EMPRESA</Text>
<View style={[styles.selectorContainer, { borderColor: cores.borda }]}>
<ScrollView nestedScrollEnabled style={{maxHeight: 200}}>
{Object.keys(empresasAgrupadas).map(c => (
<View key={c}>
<Text style={[styles.groupHead, { color: cores.azul, backgroundColor: cores.azulSuave }]}>{c}</Text>
{empresasAgrupadas[c].map(emp => (
<TouchableOpacity
key={emp.id}
style={[styles.item, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]}
onPress={() => setEmpresaSelecionada(emp)}
>
<Text style={{color: empresaSelecionada?.id === emp.id ? cores.azul : cores.texto, fontWeight: empresaSelecionada?.id === emp.id ? 'bold' : '400'}}>
{emp.nome}
</Text>
{empresaSelecionada?.id === emp.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
</TouchableOpacity>
))}
</View>
))}
</ScrollView>
</View>
</View>
) : (
<View>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Passo 2: Detalhes</Text>
<View style={[styles.confirmBox, { borderColor: cores.border }]}>
<Text style={styles.confirmTitle}>DURAÇÃO E HORAS</Text>
<View style={{flexDirection: 'row', gap: 10, marginBottom: 10}}>
<TextInput style={[styles.editInput, {flex:1, color: cores.texto}]} value={dataInicio} onChangeText={setDataInicio} placeholder="Início (AAAA-MM-DD)"/>
<TextInput style={[styles.editInput, {flex:1, color: cores.texto}]} value={dataFim} onChangeText={setDataFim} placeholder="Fim (AAAA-MM-DD)"/>
<View style={{ gap: 16 }}>
<View style={[styles.confirmBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.sectionLabel, { color: cores.azul }]}>DURAÇÃO</Text>
<View style={{flexDirection: 'row', gap: 10, marginTop: 8}}>
<View style={{flex:1}}>
<Text style={styles.miniLabel}>INÍCIO</Text>
<TextInput style={[styles.editInput, { color: cores.texto }]} value={dataInicio} onChangeText={setDataInicio} placeholder="2024-09-01"/>
</View>
<View style={{flex:1}}>
<Text style={styles.miniLabel}>FIM</Text>
<TextInput style={[styles.editInput, { color: cores.texto }]} value={dataFim} onChangeText={setDataFim} placeholder="2025-06-30"/>
</View>
</View>
<View style={[styles.badge, { backgroundColor: cores.azul, marginTop: 12, alignSelf: 'flex-start' }]}>
<Text style={[styles.badgeText, { color: '#fff' }]}>Total: {totalHorasDiarias}</Text>
</View>
<View style={styles.badgeHoras}><Text style={styles.badgeText}>Total Diário: {totalHorasDiarias}</Text></View>
</View>
<View style={[styles.confirmBox, { borderColor: cores.border, marginTop: 15 }]}>
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text style={styles.confirmTitle}>HORÁRIOS</Text>
<TouchableOpacity onPress={() => setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}><Ionicons name="add-circle" size={24} color={cores.azul}/></TouchableOpacity>
<View style={[styles.confirmBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center'}}>
<Text style={[styles.sectionLabel, { color: cores.azul }]}>HORÁRIOS</Text>
<TouchableOpacity onPress={() => setHorarios([...horarios, { periodo: 'Manhã', hora_inicio: '09:00', hora_fim: '13:00' }])}>
<Ionicons name="add-circle" size={26} color={cores.azul}/>
</TouchableOpacity>
</View>
{horarios.map((h, i) => (
<View key={i} style={styles.horarioRow}>
<TextInput style={[styles.miniInput, {color: cores.texto, flex: 1.2}]} value={h.periodo} onChangeText={(t) => { const n = [...horarios]; n[i].periodo = t; setHorarios(n); }} />
<TextInput style={[styles.miniInput, {color: cores.texto}]} value={h.hora_inicio} onChangeText={(t) => { const n = [...horarios]; n[i].hora_inicio = t; setHorarios(n); }} />
<Text style={{color: cores.textoSecundario}}>às</Text>
<Text style={{color: cores.secundario, fontSize: 10}}>ATÉ</Text>
<TextInput style={[styles.miniInput, {color: cores.texto}]} value={h.hora_fim} onChangeText={(t) => { const n = [...horarios]; n[i].hora_fim = t; setHorarios(n); }} />
<TouchableOpacity onPress={() => setHorarios(horarios.filter((_, idx) => idx !== i))}><Ionicons name="close-circle" size={20} color={cores.vermelho} /></TouchableOpacity>
<TouchableOpacity onPress={() => setHorarios(horarios.filter((_, idx) => idx !== i))}>
<Ionicons name="trash" size={18} color={cores.vermelho} />
</TouchableOpacity>
</View>
))}
</View>
<View style={[styles.confirmBox, { borderColor: cores.border, marginTop: 15 }]}>
<Text style={styles.confirmTitle}>TUTOR</Text>
<TextInput style={[styles.editInput, {color: cores.texto}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome"/>
<TextInput style={[styles.editInput, {color: cores.texto, marginTop: 8}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/>
</View>
<View style={styles.modalFooter}>
<TouchableOpacity
onPress={() => editandoEstagio ? handleFecharModal() : setPasso(1)}
style={[styles.btnModal, {backgroundColor: cores.textoSecundario}]}
>
<Text style={{color:'#fff'}}>{editandoEstagio ? 'Cancelar' : 'Voltar'}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={salvarEstagio} style={[styles.btnModal, {backgroundColor: cores.azul}]}><Text style={{color:'#fff'}}>Gravar</Text></TouchableOpacity>
<View style={[styles.confirmBox, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.sectionLabel, { color: cores.azul }]}>DADOS DO TUTOR</Text>
<TextInput style={[styles.editInput, {color: cores.texto, marginTop: 8}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t}:p)} placeholder="Nome do Tutor"/>
<TextInput style={[styles.editInput, {color: cores.texto, marginTop: 12}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t}:p)} keyboardType="phone-pad" placeholder="Telefone"/>
</View>
</View>
)}
</ScrollView>
<View style={styles.modalFooter}>
{passo === 2 && !editandoEstagio && (
<TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.azulSuave }]}>
<Text style={{color: cores.azul, fontWeight: '700'}}>VOLTAR</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => {
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 }]}
>
<Text style={{color:'#fff', fontWeight: '800'}}>
{passo === 1 ? "PRÓXIMO" : "GRAVAR"}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
</View>
);
}
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' },
});

View File

@@ -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<Empresa>({ ...empresaOriginal });
const [alunos, setAlunos] = useState<AlunoVinculado[]>([]);
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 (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={cores.fundo} translucent={false} />
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
<View style={styles.header}>
<TouchableOpacity style={[styles.btnVoltar, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
<TouchableOpacity style={[styles.btnCircle, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]} numberOfLines={1}>
{empresaLocal.nome || 'Detalhes'}
Detalhes da Entidade
</Text>
<View style={styles.headerAcoes}>
<TouchableOpacity
style={[styles.btnAcao, { backgroundColor: editando ? cores.vermelho : '#ffc107' }]}
onPress={() => setEditando(!editando)}
>
<Ionicons name={editando ? "close" : "pencil"} size={20} color={editando ? "#fff" : "#000"} />
</TouchableOpacity>
<TouchableOpacity style={[styles.btnAcao, { backgroundColor: cores.vermelho }]} onPress={handleDelete}>
<Ionicons name="trash" size={20} color="#fff" />
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.btnCircle, { backgroundColor: editando ? cores.vermelho : cores.card }]}
onPress={() => setEditando(!editando)}
>
<Ionicons name={editando ? "close" : "pencil"} size={20} color={editando ? "#fff" : cores.azul} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.container}>
{/* CARD DE DADOS */}
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.tituloCard, { color: cores.azul }]}>Informações da Empresa</Text>
{[
{ label: 'Nome', key: 'nome' },
{ label: 'Curso', key: 'curso' },
{ label: 'Morada', key: 'morada' },
{ label: 'Tutor', key: 'tutor_nome' },
{ label: 'Telefone', key: 'tutor_telefone' },
].map((item) => (
<View key={item.key} style={styles.campoWrapper}>
<Text style={[styles.label, { color: cores.textoSecundario }]}>{item.label}</Text>
{editando ? (
<TextInput
style={[styles.input, { color: cores.texto, borderColor: cores.textoSecundario }]}
value={(empresaLocal as any)[item.key]}
onChangeText={(v) => setEmpresaLocal(prev => ({ ...prev, [item.key]: v }))}
/>
) : (
<Text style={[styles.valor, { color: cores.texto }]}>{(empresaLocal as any)[item.key] || '---'}</Text>
)}
</View>
))}
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Informação da Empresa</Text>
<InfoItem label="Nome" value={empresaLocal.nome} icon="business" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} />
<InfoItem label="Curso" value={empresaLocal.curso} icon="book" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} />
<InfoItem label="Morada" value={empresaLocal.morada} icon="location" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} />
</View>
{editando && (
<TouchableOpacity style={[styles.saveButton, { backgroundColor: cores.azul }]} onPress={handleSave} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Confirmar Alterações</Text>}
</TouchableOpacity>
)}
{/* ESTATÍSTICAS COM LISTA DE ALUNOS */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.tituloCard, { color: cores.azul }]}>Alunos em Estágio</Text>
{empresaLocal.alunos && empresaLocal.alunos.length > 0 ? (
empresaLocal.alunos.map((aluno, index) => (
<View key={index} style={styles.alunoRow}>
<Ionicons name="person" size={16} color={cores.azul} style={{ marginRight: 8 }} />
<Text style={[styles.valor, { color: cores.texto }]}>{aluno}</Text>
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Contacto do Tutor</Text>
<InfoItem label="Nome" value={empresaLocal.tutor_nome} icon="person" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} />
<InfoItem label="Telefone" value={empresaLocal.tutor_telefone} icon="call" editable={editando}
onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" />
</View>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<View style={styles.alunosHeader}>
<Text style={[styles.sectionLabel, { color: cores.secundario, marginBottom: 0 }]}>Alunos em Estágio</Text>
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
<Text style={[styles.badgeText, { color: cores.azul }]}>
{loadingAlunos ? '...' : alunos.length}
</Text>
</View>
</View>
{loadingAlunos ? (
<ActivityIndicator size="small" color={cores.azul} style={{ marginVertical: 10 }} />
) : alunos.length > 0 ? (
alunos.map((aluno, index) => (
<View
key={index}
style={[styles.alunoItem, { borderBottomColor: cores.borda, borderBottomWidth: index === alunos.length - 1 ? 0 : 1 }]}
>
<View style={[styles.alunoAvatar, { backgroundColor: cores.azulSuave }]}>
<Text style={{ color: cores.azul, fontWeight: 'bold', fontSize: 12 }}>{aluno.nome.charAt(0).toUpperCase()}</Text>
</View>
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
</View>
))
) : (
<Text style={[styles.valor, { color: cores.textoSecundario, textAlign: 'center' }]}>
Nenhum aluno associado.
</Text>
<View style={styles.emptyAlunos}>
<Ionicons name="people-outline" size={30} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 5, fontSize: 13 }}>Nenhum aluno associado</Text>
</View>
)}
<View style={{ marginTop: 20, borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 10 }}>
<Text style={[styles.label, { color: cores.textoSecundario }]}>Total de Alunos</Text>
<Text style={[styles.valor, { color: cores.verde, fontSize: 20 }]}>
{empresaLocal.alunos?.length || 0}
</Text>
</View>
</View>
{editando ? (
<TouchableOpacity style={[styles.btnPrincipal, { backgroundColor: cores.azul }]} onPress={handleSave} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.btnPrincipalText}>Guardar Alterações</Text>}
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.btnDelete, { backgroundColor: cores.vermelhoSuave }]} onPress={handleDelete}>
<Ionicons name="trash-outline" size={20} color={cores.vermelho} />
<Text style={[styles.btnDeleteText, { color: cores.vermelho }]}>Remover Entidade</Text>
</TouchableOpacity>
)}
</ScrollView>
</SafeAreaView>
</View>
);
});
export default DetalhesEmpresa;
const InfoItem = ({ label, value, icon, editable, onChangeText, cores, ...props }: InfoItemProps) => (
<View style={styles.infoWrapper}>
<View style={[styles.infoIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={icon} size={18} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.infoLabel, { color: cores.secundario }]}>{label}</Text>
{editable ? (
<TextInput
style={[styles.infoInput, { color: cores.texto, borderBottomColor: cores.azul }]}
value={value}
onChangeText={onChangeText}
{...props}
/>
) : (
<Text style={[styles.infoValue, { color: cores.texto }]}>{value || '---'}</Text>
)}
</View>
</View>
);
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 }
});
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;

View File

@@ -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<Empresa[]>([]);
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 (
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={cores.fundo}
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
translucent
backgroundColor="transparent"
/>
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: 15 }}>
<Text style={[styles.headerTitle, { color: cores.texto }]}>Empresas</Text>
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
{empresas.length} entidades no total
</Text>
</View>
<TouchableOpacity
style={[styles.addBtn, { backgroundColor: cores.azul }]}
onPress={() => setModalVisible(true)}
>
<Ionicons name="add" size={26} color="#fff" />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>
Empresas
</Text>
<View style={{ width: 40 }} />
</View>
{/* SEARCH */}
<View style={styles.searchContainer}>
<Ionicons name="search" size={20} color={cores.textoSecundario} style={styles.searchIcon} />
<TextInput
style={[styles.search, { backgroundColor: cores.card, color: cores.texto }]}
placeholder="Pesquisar empresa..."
placeholderTextColor={cores.textoSecundario}
value={search}
onChangeText={setSearch}
/>
{/* SEARCH BAR */}
<View style={styles.searchSection}>
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.secundario} />
<TextInput
style={[styles.searchInput, { color: cores.texto }]}
placeholder="Pesquisar por nome ou curso..."
placeholderTextColor={cores.secundario}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
{/* BOTÃO NOVA EMPRESA */}
<TouchableOpacity
style={[styles.btnCriar, { backgroundColor: cores.azul }]}
onPress={() => setModalVisible(true)}
>
<Ionicons name="add-circle-outline" size={20} color="#fff" />
<Text style={styles.txtBtnCriar}>Nova Empresa</Text>
</TouchableOpacity>
{/* LISTA */}
{/* CONTEÚDO / LISTA */}
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 40 }} />
<View style={styles.loadingCenter}>
<ActivityIndicator size="large" color={cores.azul} />
</View>
) : (
<FlatList
data={filteredEmpresas}
<SectionList
sections={secoesAgrupadas}
keyExtractor={item => item.id.toString()}
stickySectionHeadersEnabled={false}
contentContainerStyle={[
styles.listPadding,
{ paddingBottom: insets.bottom + 100 }
]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />
}
renderSectionHeader={({ section: { title } }) => (
<View style={styles.sectionHeader}>
<View style={[styles.sectionLine, { backgroundColor: cores.azul }]} />
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{title}</Text>
</View>
)}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.card, { backgroundColor: cores.card }]}
activeOpacity={0.7}
style={[styles.empresaCard, { backgroundColor: cores.card }]}
onPress={() =>
router.push({
pathname: '/Professor/Empresas/DetalhesEmpresa',
@@ -192,130 +227,122 @@ const ListaEmpresasProfessor = memo(() => {
})
}
>
<View>
<Text style={[styles.nomeEmpresa, { color: cores.azul }]}>{item.nome}</Text>
<Text style={[styles.info, { color: cores.textoSecundario }]}>
<Ionicons name="book-outline" size={14} /> {item.curso}
</Text>
<Text style={[styles.info, { color: cores.textoSecundario }]}>
<Ionicons name="person-outline" size={14} /> {item.tutor_nome}
</Text>
<View style={[styles.empresaIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="business" size={22} color={cores.azul} />
</View>
<Ionicons name="chevron-forward" size={20} color={cores.textoSecundario} />
<View style={styles.empresaInfo}>
<Text style={[styles.empresaNome, { color: cores.texto }]}>{item.nome}</Text>
<View style={styles.tutorRow}>
<Ionicons name="person-outline" size={14} color={cores.secundario} />
<Text style={[styles.tutorText, { color: cores.secundario }]}>{item.tutor_nome}</Text>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.borda} />
</TouchableOpacity>
)}
contentContainerStyle={styles.listContent}
ListEmptyComponent={() => (
<View style={styles.emptyContainer}>
<Ionicons name="search-circle-outline" size={60} color={cores.borda} />
<Text style={{ color: cores.secundario, marginTop: 10 }}>Nenhuma empresa encontrada.</Text>
</View>
)}
/>
)}
</SafeAreaView>
{/* MODAL */}
{/* MODAL DE CRIAÇÃO */}
<Modal visible={modalVisible} animationType="slide" transparent>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>
Nova Empresa
</Text>
<ScrollView>
<Input label="Nome" value={nome} onChangeText={setNome} cores={cores} />
<Input label="Morada" value={morada} onChangeText={setMorada} cores={cores} />
<Input label="Curso" value={curso} onChangeText={setCurso} cores={cores} />
<Input label="Tutor" value={tutorNome} onChangeText={setTutorNome} cores={cores} />
<Input
label="Telefone"
value={tutorTelefone}
onChangeText={setTutorTelefone}
keyboardType="phone-pad"
cores={cores}
/>
</ScrollView>
<View style={styles.modalButtons}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalOverlay}
>
<View style={[
styles.modalContent,
{ backgroundColor: cores.card, paddingBottom: Platform.OS === 'ios' ? insets.bottom + 20 : 30 }
]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: cores.texto }]}>Registar Empresa</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Text style={{ color: '#dc3545', fontWeight: 'bold' }}>
Cancelar
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btnConfirmar, { backgroundColor: cores.azul }]}
onPress={criarEmpresa}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>
Criar
</Text>
<Ionicons name="close-circle" size={28} color={cores.secundario} />
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
<ModernInput label="Nome da Entidade" icon="business-outline" value={nome} onChangeText={setNome} cores={cores} />
<ModernInput label="Morada" icon="location-outline" value={morada} onChangeText={setMorada} cores={cores} />
<ModernInput label="Curso (ex: GPSI, Multimédia)" icon="book-outline" value={curso} onChangeText={setCurso} cores={cores} />
<ModernInput label="Tutor de Empresa" icon="person-outline" value={tutorNome} onChangeText={setTutorNome} cores={cores} />
<ModernInput
label="Telefone do Tutor"
icon="call-outline"
value={tutorTelefone}
onChangeText={setTutorTelefone}
keyboardType="phone-pad"
cores={cores}
/>
<TouchableOpacity
style={[styles.saveBtn, { backgroundColor: cores.azul }]}
onPress={criarEmpresa}
>
<Text style={styles.saveBtnText}>Guardar Empresa</Text>
</TouchableOpacity>
</ScrollView>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</View>
);
});
export default ListaEmpresasProfessor;
/* INPUT */
const Input = ({ label, cores, ...props }: any) => (
<View style={{ marginBottom: 10 }}>
<Text style={{ color: cores.textoSecundario, marginBottom: 4 }}>
{label}
</Text>
<TextInput
{...props}
style={{
backgroundColor: cores.fundo,
color: cores.texto,
padding: 12,
borderRadius: 10
}}
placeholderTextColor={cores.textoSecundario}
/>
const ModernInput = ({ label, icon, cores, ...props }: any) => (
<View style={styles.inputWrapper}>
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
<View style={[styles.inputContainer, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 10 }} />
<TextInput
{...props}
style={[styles.textInput, { color: cores.texto }]}
placeholderTextColor={cores.secundario}
/>
</View>
</View>
);
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;

View File

@@ -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<PerfilData | null>(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 (
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor="transparent"
translucent
/>
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<ScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* HEADER */}
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="arrow-back-outline" size={26} color={cores.texto} />
<TouchableOpacity style={[styles.backBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
<View style={{ width: 26 }} />
<TouchableOpacity
style={[styles.editBtn, { backgroundColor: editando ? cores.azul : cores.card }]}
onPress={() => editando ? guardarPerfil() : setEditando(true)}
>
<Ionicons name={editando ? "checkmark" : "create-outline"} size={20} color={editando ? "#fff" : cores.azul} />
</TouchableOpacity>
</View>
<View style={styles.header}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Ionicons name="person" size={48} color="#fff" />
</View>
<Text style={[styles.name, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.role, { color: cores.textoSecundario }]}>
{perfil?.curso || 'Sem curso definido'}
</Text>
</View>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<InfoField
label="Nome Completo"
value={perfil?.nome || ''}
editable={editando}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)}
cores={cores}
/>
<InfoField
label="Área / Curso"
value={perfil?.curso || ''}
editable={editando}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)}
cores={cores}
/>
<InfoField label="Email (Login)" value={perfil?.email || ''} editable={false} cores={cores} />
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<InfoField
label="Nº Escola"
value={perfil?.n_escola || ''}
editable={editando}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)}
cores={cores}
/>
{/* AVATAR SECTION */}
<View style={styles.profileHeader}>
<View style={[styles.avatarContainer, { borderColor: cores.azulSuave }]}>
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
</View>
</View>
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
<Text style={[styles.userRole, { color: cores.secundario }]}>
{perfil?.curso || 'Professor'}
</Text>
</View>
<InfoField
label="Telefone"
value={perfil?.telefone || ''}
editable={editando}
onChange={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)}
cores={cores}
/>
</View>
{/* INFO CARD */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<ModernInput
label="Nome Completo"
icon="person-outline"
value={perfil?.nome || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)}
cores={cores}
/>
<View style={styles.actions}>
{editando ? (
<TouchableOpacity style={[styles.primaryButton, { backgroundColor: cores.azul }]} onPress={guardarPerfil}>
<Ionicons name="save-outline" size={20} color="#fff" />
<Text style={styles.primaryText}>Guardar Alterações</Text>
<ModernInput
label="Área / Curso"
icon="book-outline"
value={perfil?.curso || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)}
cores={cores}
/>
<ModernInput
label="E-mail"
icon="mail-outline"
value={perfil?.email || ''}
editable={false}
cores={cores}
/>
<View style={styles.row}>
<View style={{ flex: 1, marginRight: 10 }}>
<ModernInput
label="Nº Escola"
icon="id-card-outline"
value={perfil?.n_escola || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)}
cores={cores}
/>
</View>
<View style={{ flex: 1.5 }}>
<ModernInput
label="Telefone"
icon="call-outline"
value={perfil?.telefone || ''}
editable={editando}
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)}
keyboardType="phone-pad"
cores={cores}
/>
</View>
</View>
</View>
{/* ACTIONS */}
<View style={styles.actionsContainer}>
<TouchableOpacity
style={[styles.menuItem, { backgroundColor: cores.card }]}
onPress={() => router.push('/Professor/redefenirsenha2')}
>
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
<Ionicons name="chevron-forward" size={18} color={cores.borda} />
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.actionButton, { backgroundColor: cores.card }]} onPress={() => setEditando(true)}>
<Ionicons name="create-outline" size={20} color={cores.azul} />
<Text style={[styles.actionText, { color: cores.texto }]}>Editar Perfil</Text>
<TouchableOpacity
style={[styles.menuItem, { backgroundColor: cores.card }]}
onPress={terminarSessao}
>
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
</View>
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
{editando && (
<TouchableOpacity
style={[styles.cancelBtn]}
onPress={() => { setEditando(false); carregarPerfil(); }}
>
<Text style={[styles.cancelText, { color: cores.secundario }]}>Cancelar Edição</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={[styles.actionButton, { backgroundColor: cores.card }]}
onPress={() => router.push('/Professor/redefenirsenha2')}
>
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
<Text style={[styles.actionText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, { backgroundColor: cores.card }]} onPress={terminarSessao}>
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
<Text style={[styles.actionText, { color: cores.vermelho }]}>Terminar Sessão</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
function InfoField({ label, value, editable, onChange, cores }: any) {
return (
<View style={styles.infoField}>
<Text style={[styles.label, { color: cores.textoSecundario }]}>{label}</Text>
{editable ? (
<TextInput
value={value}
onChangeText={onChange}
style={[styles.input, { backgroundColor: cores.inputBg, color: cores.texto, borderColor: cores.border }]}
/>
) : (
<Text style={[styles.value, { color: cores.texto }]}>{value || 'Não definido'}</Text>
)}
</ScrollView>
</SafeAreaView>
</View>
);
}
const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
<View style={styles.inputWrapper}>
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
<View style={[
styles.inputContainer,
{
backgroundColor: cores.fundo,
borderColor: editable ? cores.azul : cores.borda,
opacity: editable ? 1 : 0.7
}
]}>
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 10 }} />
<TextInput
{...props}
editable={editable}
style={[styles.textInput, { color: cores.texto }]}
placeholderTextColor={cores.secundario}
/>
</View>
</View>
);
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' }
});

View File

@@ -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 (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
<View style={styles.header}>
<TouchableOpacity
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{ width: 40 }} />
</View>
<View style={styles.container}>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.subtituloSecao, { color: cores.azul }]}>Preferências</Text>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name="notifications-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Notificações</Text>
</View>
<Switch
value={notificacoes}
onValueChange={setNotificacoes}
trackColor={{ false: '#767577', true: cores.azul }}
/>
</View>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name={isDarkMode ? "moon" : "sunny-outline"} size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Modo escuro</Text>
</View>
<Switch
value={isDarkMode}
onValueChange={toggleTheme} // Chamada direta otimizada pelo ThemeContext
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={isDarkMode ? '#fff' : '#f4f3f4'}
/>
</View>
<Text style={[styles.subtituloSecao, { color: cores.azul, marginTop: 25 }]}>Suporte e Contactos</Text>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail}>
<View style={styles.iconTexto}>
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Direção</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>epvc@epvc.pt</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail2}>
<View style={styles.iconTexto}>
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Secretaria</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>secretaria@epvc.pt</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirTelefone}>
<View style={styles.iconTexto}>
<Ionicons name="call-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Ligar para a Escola</Text>
</View>
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>252 641 805</Text>
</TouchableOpacity>
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
<View style={styles.iconTexto}>
<Ionicons name="information-circle-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
<Text style={{ color: cores.texto }}>Versão da app</Text>
</View>
<Text style={{ color: cores.textoSecundario }}>26.1.10</Text>
</View>
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.linha, { borderBottomWidth: 0 }]}
onPress={handleLogout}
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<View style={styles.iconTexto}>
<Ionicons name="log-out-outline" size={22} color={cores.sair} style={{marginRight: 10}} />
<Text style={{ color: cores.sair, fontWeight: '600' }}>Terminar Sessão</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={cores.textoSecundario} />
<Ionicons name="arrow-back" size={22} color={cores.texto} />
</TouchableOpacity>
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
<View style={{ width: 42 }} />
</View>
</View>
</SafeAreaView>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* SECÇÃO PREFERÊNCIAS */}
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Preferências</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="notifications-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações</Text>
<Switch
value={notificacoes}
onValueChange={setNotificacoes}
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={Platform.OS === 'ios' ? undefined : '#f4f3f4'}
/>
</View>
<View style={styles.item}>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name={isDarkMode ? "moon-outline" : "sunny-outline"} size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Modo Escuro</Text>
<Switch
value={isDarkMode}
onValueChange={toggleTheme}
trackColor={{ false: '#767577', true: cores.azul }}
thumbColor={Platform.OS === 'ios' ? undefined : '#f4f3f4'}
/>
</View>
</View>
{/* SECÇÃO SUPORTE */}
<Text style={[styles.sectionTitle, { color: cores.secundario, marginTop: 25 }]}>Suporte e Contactos</Text>
<View style={[styles.card, { backgroundColor: cores.card }]}>
<TouchableOpacity
style={[styles.item, { borderBottomColor: cores.borda }]}
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="business-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>Direção</Text>
<Text style={{ color: cores.secundario, fontSize: 12 }}>epvc@epvc.pt</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.item, { borderBottomColor: cores.borda }]}
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="mail-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>Secretaria</Text>
<Text style={{ color: cores.secundario, fontSize: 12 }}>secretaria@epvc.pt</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</TouchableOpacity>
<TouchableOpacity
style={styles.item}
onPress={() => abrirURL('tel:252641805')}
>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="call-outline" size={20} color={cores.azul} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>Telefone</Text>
<Text style={{ color: cores.secundario, fontSize: 12 }}>252 641 805</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
</TouchableOpacity>
</View>
{/* SECÇÃO INFO & SAIR */}
<View style={[styles.card, { backgroundColor: cores.card, marginTop: 25 }]}>
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
<Ionicons name="information-circle-outline" size={20} color={cores.azul} />
</View>
<Text style={[styles.itemTexto, { color: cores.texto }]}>Versão da App</Text>
<Text style={{ color: cores.secundario, fontWeight: '600' }}>26.3.11</Text>
</View>
<TouchableOpacity
style={styles.item}
onPress={handleLogout}
>
<View style={[styles.iconBox, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="log-out-outline" size={22} color={cores.vermelho} />
</View>
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '700' }]}>Terminar Sessão</Text>
<Ionicons name="chevron-forward" size={16} color={cores.vermelho} />
</TouchableOpacity>
</View>
<Text style={[styles.footerText, { color: cores.secundario }]}>Escola Profissional de Vila do Conde © 2026</Text>
</ScrollView>
</SafeAreaView>
</View>
);
});
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;

View File

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