aula lp - atualização
This commit is contained in:
3
app.json
3
app.json
@@ -9,7 +9,8 @@
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.anonymous.estagios-pap"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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' }
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user