This commit is contained in:
2026-01-28 10:33:31 +00:00
parent 50db3a0902
commit 1f2b41a9c3
5 changed files with 618 additions and 236 deletions

View File

@@ -174,11 +174,11 @@ const AlunoHome = memo(() => {
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<ScrollView contentContainerStyle={styles.container}>
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.push('/perfil')}>
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}>
<Ionicons name="person-circle-outline" size={32} color={themeStyles.texto} />
</TouchableOpacity>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
<TouchableOpacity onPress={() => router.push('/definicoes')}>
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')}>
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
</TouchableOpacity>
</View>

View File

@@ -2,22 +2,21 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
Modal,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
Modal,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { WebView } from 'react-native-webview';
import { useTheme } from '../../../themecontext'; // <- seu contexto de tema
import { useTheme } from '../../../themecontext';
interface Falta {
dia: string;
motivo?: string;
pdfUrl?: string;
}
@@ -33,16 +32,22 @@ const alunosData: Aluno[] = [
id: 1,
nome: 'João Silva',
faltas: [
{ dia: '2026-01-20', motivo: 'Doença', pdfUrl: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' },
{ dia: '2026-01-22', motivo: 'Atraso' },
{
dia: '2026-01-20',
pdfUrl: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
},
{ dia: '2026-01-22' },
],
},
{
id: 2,
nome: 'Maria Fernandes',
faltas: [
{ dia: '2026-01-21', motivo: 'Consulta médica' },
{ dia: '2026-01-23', motivo: 'Doença', pdfUrl: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' },
{ dia: '2026-01-21' },
{
dia: '2026-01-23',
pdfUrl: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
},
],
},
];
@@ -82,18 +87,22 @@ export default function FaltasAlunos() {
<ScrollView contentContainerStyle={styles.container}>
{/* Top Bar */}
<View style={styles.topBar}>
<TouchableOpacity onPress={() => {
if (alunoSelecionado) {
setAlunoSelecionado(null); // voltar para lista de alunos
} else {
router.back(); // voltar para menu
}
}}>
<TouchableOpacity
onPress={() => {
if (alunoSelecionado) {
setAlunoSelecionado(null);
} else {
router.back();
}
}}
>
<Ionicons name="arrow-back-outline" size={26} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.topTitle, { color: cores.texto }]}>
{!alunoSelecionado ? 'Faltas dos Alunos' : alunoSelecionado.nome}
</Text>
<View style={{ width: 26 }} />
</View>
@@ -107,8 +116,14 @@ export default function FaltasAlunos() {
onPress={() => setAlunoSelecionado(aluno)}
>
<Ionicons name="person-outline" size={28} color={cores.azul} />
<Text style={[styles.alunoName, { color: cores.texto }]}>{aluno.nome}</Text>
<Ionicons name="chevron-forward-outline" size={20} color={cores.textoSecundario} />
<Text style={[styles.alunoName, { color: cores.texto }]}>
{aluno.nome}
</Text>
<Ionicons
name="chevron-forward-outline"
size={20}
color={cores.textoSecundario}
/>
</TouchableOpacity>
))}
</View>
@@ -118,17 +133,38 @@ export default function FaltasAlunos() {
{alunoSelecionado && (
<View style={{ marginTop: 10 }}>
{alunoSelecionado.faltas.map((falta, idx) => (
<View key={idx} style={[styles.faltaCard, { backgroundColor: cores.card }]}>
<View
key={idx}
style={[styles.faltaCard, { backgroundColor: cores.card }]}
>
<View style={{ flex: 1 }}>
<Text style={[styles.dia, { color: cores.azul }]}>{falta.dia}</Text>
<Text style={[styles.motivo, { color: cores.texto }]}>{falta.motivo || 'Sem motivo'}</Text>
<Text style={[styles.status, { color: falta.pdfUrl ? cores.verde : cores.vermelho }]}>
{falta.pdfUrl ? 'Justificada com PDF' : 'Não justificada'}
<Text style={[styles.dia, { color: cores.azul }]}>
{falta.dia}
</Text>
<Text
style={[
styles.status,
{
color: falta.pdfUrl
? cores.verde
: cores.vermelho,
},
]}
>
{falta.pdfUrl
? 'Falta justificada'
: 'Falta não justificada'}
</Text>
</View>
{falta.pdfUrl && (
<TouchableOpacity onPress={() => verPdf(falta)}>
<Ionicons name="document-text-outline" size={24} color={cores.azul} />
<Ionicons
name="document-text-outline"
size={24}
color={cores.azul}
/>
</TouchableOpacity>
)}
</View>
@@ -140,13 +176,21 @@ export default function FaltasAlunos() {
{/* Modal PDF */}
<Modal visible={pdfModalVisible} animationType="slide">
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }}>
<View style={[styles.pdfHeader, { borderBottomColor: cores.textoSecundario }]}>
<View
style={[
styles.pdfHeader,
{ borderBottomColor: cores.textoSecundario },
]}
>
<TouchableOpacity onPress={() => setPdfModalVisible(false)}>
<Ionicons name="close-outline" size={28} color={cores.azul} />
</TouchableOpacity>
<Text style={[styles.pdfTitle, { color: cores.texto }]}>Visualizador de PDF</Text>
<Text style={[styles.pdfTitle, { color: cores.texto }]}>
Visualizador de PDF
</Text>
<View style={{ width: 28 }} />
</View>
{pdfUrl && <WebView source={{ uri: pdfUrl }} style={{ flex: 1 }} />}
</SafeAreaView>
</Modal>
@@ -155,10 +199,23 @@ export default function FaltasAlunos() {
}
const styles = StyleSheet.create({
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
safe: {
flex: 1,
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
},
container: { padding: 20, paddingBottom: 40 },
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 },
topTitle: { fontSize: 20, fontWeight: '700', textAlign: 'center', flex: 1 },
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16,
},
topTitle: {
fontSize: 20,
fontWeight: '700',
textAlign: 'center',
flex: 1,
},
alunoCard: {
flexDirection: 'row',
alignItems: 'center',
@@ -171,7 +228,12 @@ const styles = StyleSheet.create({
shadowRadius: 4,
elevation: 2,
},
alunoName: { fontSize: 16, fontWeight: '600', marginLeft: 12, flex: 1 },
alunoName: {
fontSize: 16,
fontWeight: '600',
marginLeft: 12,
flex: 1,
},
faltaCard: {
flexDirection: 'row',
alignItems: 'center',
@@ -185,9 +247,15 @@ const styles = StyleSheet.create({
shadowRadius: 4,
elevation: 2,
},
dia: { fontSize: 14, fontWeight: '700', marginBottom: 4 },
motivo: { fontSize: 14, marginBottom: 4 },
status: { fontSize: 13, fontWeight: '600' },
dia: {
fontSize: 14,
fontWeight: '700',
marginBottom: 6,
},
status: {
fontSize: 13,
fontWeight: '600',
},
pdfHeader: {
flexDirection: 'row',
alignItems: 'center',
@@ -195,5 +263,8 @@ const styles = StyleSheet.create({
padding: 16,
borderBottomWidth: 1,
},
pdfTitle: { fontSize: 18, fontWeight: '700' },
pdfTitle: {
fontSize: 18,
fontWeight: '700',
},
});

View File

@@ -2,6 +2,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { memo, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Platform,
SafeAreaView,
@@ -14,22 +15,38 @@ import {
View
} from 'react-native';
import { useTheme } from '../../../themecontext';
import type { Empresa } from './ListaEmpresas';
import { supabase } from '../../lib/supabase';
// Interface atualizada para incluir a lista de alunos
export interface Empresa {
id: number;
nome: string;
morada: string;
tutor_nome: string;
tutor_telefone: string;
curso: string;
alunos?: string[]; // Array com nomes dos alunos
}
const DetalhesEmpresa = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const params = useLocalSearchParams();
// Parse da empresa recebida
// Parse seguro dos dados vindos da navegação
const empresaOriginal: Empresa = useMemo(() => {
if (!params.empresa) return null as any;
if (!params.empresa) return {} as Empresa;
const str = Array.isArray(params.empresa) ? params.empresa[0] : params.empresa;
return JSON.parse(str);
try {
return JSON.parse(str);
} catch {
return {} as Empresa;
}
}, [params.empresa]);
const [empresaLocal, setEmpresaLocal] = useState<Empresa>({ ...empresaOriginal });
const [editando, setEditando] = useState(false);
const [loading, setLoading] = useState(false);
const cores = {
fundo: isDarkMode ? '#121212' : '#f1f3f5',
@@ -41,115 +58,150 @@ const DetalhesEmpresa = memo(() => {
vermelho: '#dc3545',
};
const handleSave = () => {
setEditando(false);
Alert.alert('Sucesso', 'Empresa atualizada!');
// Aqui depois substituis por update no Supabase
const handleSave = async () => {
try {
setLoading(true);
const { error } = await supabase
.from('empresas')
.update({
nome: empresaLocal.nome,
morada: empresaLocal.morada,
tutor_nome: empresaLocal.tutor_nome,
tutor_telefone: empresaLocal.tutor_telefone,
curso: empresaLocal.curso,
})
.eq('id', empresaLocal.id);
if (error) throw error;
setEditando(false);
Alert.alert('Sucesso', 'Dados atualizados!');
} catch (error: any) {
Alert.alert('Erro', error.message);
} finally {
setLoading(false);
}
};
const handleDelete = () => {
Alert.alert(
'Apagar Empresa',
'Tem a certeza que deseja apagar esta empresa?',
[
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Apagar', style: 'destructive', onPress: () => router.back() }
]
);
Alert.alert('Apagar', `Apagar ${empresaLocal.nome}?`, [
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Apagar',
style: 'destructive',
onPress: async () => {
await supabase.from('empresas').delete().eq('id', empresaLocal.id);
router.back();
}
}
]);
};
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'} backgroundColor={cores.fundo} translucent={false} />
<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 }]}>{empresaLocal.nome}</Text>
<View style={{ flexDirection: 'row', gap: 8 }}>
<TouchableOpacity style={[styles.btnAcao, { backgroundColor: '#ffc107' }]} onPress={() => setEditando(!editando)}>
<Ionicons name="pencil" size={20} color="#000" />
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<View style={styles.header}>
<TouchableOpacity style={[styles.btnVoltar, { backgroundColor: cores.card }]} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</TouchableOpacity>
<TouchableOpacity style={[styles.btnAcao, { backgroundColor: cores.vermelho }]} onPress={handleDelete}>
<Ionicons name="trash" size={20} color="#fff" />
</TouchableOpacity>
</View>
</View>
<ScrollView contentContainerStyle={styles.container}>
{/* Dados da Empresa */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.tituloCard, { color: cores.azul }]}>Dados da Empresa</Text>
{['nome', 'curso', 'morada', 'tutor', 'telefone'].map((campo) => (
<View key={campo}>
<Text style={[styles.label, { color: cores.textoSecundario }]}>
{campo.charAt(0).toUpperCase() + campo.slice(1)}
</Text>
{editando ? (
<TextInput
style={[styles.input, { color: cores.texto, borderColor: cores.textoSecundario }]}
value={(empresaLocal as any)[campo]}
onChangeText={(v) => setEmpresaLocal({ ...empresaLocal, [campo]: v })}
/>
) : (
<Text style={[styles.valor, { color: cores.texto }]}>
{(empresaLocal as any)[campo]}
</Text>
)}
</View>
))}
</View>
{/* Botão Guardar alterações */}
{editando && (
<TouchableOpacity style={[styles.saveButton, { backgroundColor: cores.azul }]} onPress={handleSave}>
<Text style={{ color: '#fff', fontWeight: 'bold', textAlign: 'center' }}>Guardar alterações</Text>
</TouchableOpacity>
)}
{/* Estatísticas */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.tituloCard, { color: cores.azul }]}>Estatísticas</Text>
<Text style={[styles.label, { color: cores.textoSecundario }]}>Total de Alunos</Text>
<Text style={[styles.valor, { color: cores.verde, fontSize: 18 }]}>
{empresaLocal.alunos?.length || 0}
<Text style={[styles.tituloGeral, { color: cores.texto }]} numberOfLines={1}>
{empresaLocal.nome || 'Detalhes'}
</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>
</View>
{/* Lista de Alunos */}
{empresaLocal.alunos && empresaLocal.alunos.length > 0 && (
<ScrollView contentContainerStyle={styles.container}>
{/* CARD DE DADOS */}
<View style={[styles.card, { backgroundColor: cores.card }]}>
<Text style={[styles.tituloCard, { color: cores.azul }]}>Alunos na Empresa</Text>
{empresaLocal.alunos.map((aluno, index) => (
<Text key={index} style={[styles.valor, { color: cores.texto, marginVertical: 2 }]}>
{aluno}
</Text>
<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>
))}
</View>
)}
</ScrollView>
</SafeAreaView>
{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>
</View>
))
) : (
<Text style={[styles.valor, { color: cores.textoSecundario, textAlign: 'center' }]}>
Nenhum aluno associado.
</Text>
)}
<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>
</ScrollView>
</SafeAreaView>
</View>
);
});
export default DetalhesEmpresa;
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 },
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: 40, height: 40, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
tituloGeral: { fontSize: 22, fontWeight: 'bold' },
container: { padding: 20, gap: 20, paddingBottom: 40 },
card: { padding: 20, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
tituloCard: { fontSize: 18, fontWeight: 'bold', textAlign: 'center', marginBottom: 10, borderBottomWidth: 1, paddingBottom: 8 },
label: { marginTop: 12, fontSize: 13 },
valor: { fontSize: 16, fontWeight: '600' },
input: { borderWidth: 1, borderRadius: 10, padding: 10, marginTop: 2 },
saveButton: { padding: 16, borderRadius: 10, marginTop: 16 },
});
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 }
});

View File

@@ -1,136 +1,321 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { memo, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
FlatList, Platform, SafeAreaView, StatusBar,
StyleSheet, Text, TextInput, TouchableOpacity, View
FlatList,
Modal,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { useTheme } from '../../../themecontext';
import { supabase } from '../../lib/supabase';
export interface Empresa {
id: number;
nome: string;
morada: string;
tutor: string;
telefone: string;
tutor_nome: string;
tutor_telefone: string;
curso: string;
alunos?: string[];
}
const initialEmpresasData: Empresa[] = [
{ id: 1, nome: 'Empresa X', morada: 'Rua das Flores, 12', tutor: 'João Santos', telefone: '912345678', curso: 'Técnico de Informática', alunos: ['João Silva'] },
{ id: 2, nome: 'Empresa W', morada: 'Av. Central, 45', tutor: 'Ana Costa', telefone: '912345679', curso: 'Técnico de Design', alunos: ['Maria Fernandes'] },
];
const ListaEmpresasProfessor = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const [search, setSearch] = useState('');
const [empresas, setEmpresas] = useState<Empresa[]>(initialEmpresasData);
const cores = {
const [search, setSearch] = useState('');
const [empresas, setEmpresas] = useState<Empresa[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// MODAL + FORM
const [modalVisible, setModalVisible] = useState(false);
const [nome, setNome] = useState('');
const [morada, setMorada] = useState('');
const [tutorNome, setTutorNome] = useState('');
const [tutorTelefone, setTutorTelefone] = useState('');
const [curso, setCurso] = useState('');
const cores = useMemo(() => ({
fundo: isDarkMode ? '#121212' : '#f1f3f5',
card: isDarkMode ? '#1e1e1e' : '#fff',
texto: isDarkMode ? '#fff' : '#000',
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
azul: '#0d6efd',
}), [isDarkMode]);
const fetchEmpresas = async () => {
try {
setLoading(true);
const { data, error } = await supabase
.from('empresas')
.select('*')
.order('nome', { ascending: true });
if (error) throw error;
setEmpresas(data || []);
} catch (error: any) {
Alert.alert('Erro ao carregar', error.message);
} finally {
setLoading(false);
setRefreshing(false);
}
};
// Filtrar empresas
useEffect(() => {
fetchEmpresas();
}, []);
const onRefresh = useCallback(() => {
setRefreshing(true);
fetchEmpresas();
}, []);
const filteredEmpresas = useMemo(
() => empresas.filter(e => e.nome.toLowerCase().includes(search.toLowerCase())),
() => empresas.filter(e =>
e.nome?.toLowerCase().includes(search.toLowerCase())
),
[search, empresas]
);
// Criar nova empresa
const criarEmpresa = () => {
Alert.prompt(
'Nova Empresa',
'Insira o nome da empresa',
(nome) => {
if (!nome) return;
const nova: Empresa = {
id: Date.now(),
nome,
morada: 'Não definida',
tutor: 'Não definido',
telefone: '000000000',
curso: 'Não definido',
alunos: []
};
setEmpresas(prev => [nova, ...prev]);
router.push({
pathname: '/Professor/Empresas/DetalhesEmpresa',
params: { empresa: JSON.stringify(nova) }
});
}
);
// 👉 CRIAR EMPRESA (AGORA COM TODOS OS CAMPOS)
const criarEmpresa = async () => {
if (!nome || !morada || !tutorNome || !tutorTelefone || !curso) {
Alert.alert('Erro', 'Preenche todos os campos.');
return;
}
try {
setLoading(true);
const { data, error } = await supabase
.from('empresas')
.insert([{
nome: nome.trim(),
morada: morada.trim(),
tutor_nome: tutorNome.trim(),
tutor_telefone: tutorTelefone.trim(),
curso: curso.trim(),
}])
.select();
if (error) throw error;
setEmpresas(prev => [data![0], ...prev]);
// limpar form
setNome('');
setMorada('');
setTutorNome('');
setTutorTelefone('');
setCurso('');
setModalVisible(false);
Alert.alert('Sucesso', 'Empresa criada com sucesso!');
} catch (error: any) {
Alert.alert('Erro ao criar', error.message);
} finally {
setLoading(false);
}
};
return (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{/* Header com botão voltar */}
<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 }]}>Empresas</Text>
<View style={styles.spacer} />
</View>
{/* Input de pesquisa */}
<TextInput
style={[styles.search, { backgroundColor: cores.card, color: cores.texto }]}
placeholder="Pesquisar empresa..."
placeholderTextColor={cores.textoSecundario}
value={search}
onChangeText={setSearch}
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={cores.fundo}
/>
{/* Botão criar empresa */}
<TouchableOpacity style={[styles.btnCriar, { backgroundColor: cores.azul }]} onPress={criarEmpresa}>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>+ Criar Empresa</Text>
</TouchableOpacity>
{/* Lista de empresas */}
<FlatList
data={filteredEmpresas}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
{/* HEADER */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.card, { backgroundColor: cores.card }]}
onPress={() =>
router.push({
pathname: '/Professor/Empresas/DetalhesEmpresa',
params: { empresa: JSON.stringify(item) }
})
}
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
onPress={() => router.back()}
>
<Text style={[styles.nomeEmpresa, { color: cores.azul }]}>{item.nome}</Text>
<Text style={[styles.curso, { color: cores.textoSecundario }]}>{item.curso}</Text>
<Text style={[styles.tutor, { color: cores.textoSecundario }]}>{item.tutor}</Text>
<Ionicons name="arrow-back" size={24} color={cores.texto} />
</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}
/>
</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 */}
{loading && !refreshing ? (
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 40 }} />
) : (
<FlatList
data={filteredEmpresas}
keyExtractor={item => item.id.toString()}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />
}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.card, { backgroundColor: cores.card }]}
onPress={() =>
router.push({
pathname: '/Professor/Empresas/DetalhesEmpresa',
params: { empresa: JSON.stringify(item) }
})
}
>
<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>
<Ionicons name="chevron-forward" size={20} color={cores.textoSecundario} />
</TouchableOpacity>
)}
contentContainerStyle={styles.listContent}
/>
)}
/>
</SafeAreaView>
</SafeAreaView>
{/* MODAL */}
<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}>
<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>
</TouchableOpacity>
</View>
</View>
</View>
</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}
/>
</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 },
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' },
spacer: { width: 40 },
search: { borderRadius: 10, padding: 10, margin: 10 },
btnCriar: { marginHorizontal: 10, padding: 12, borderRadius: 10, alignItems: 'center', marginBottom: 10 },
card: { borderRadius: 10, padding: 15, marginBottom: 10, elevation: 2 },
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' },
curso: { fontSize: 14, marginTop: 2 },
tutor: { fontSize: 14, marginTop: 2 },
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',
},
});

View File

@@ -1,25 +1,79 @@
// app/index.tsx
import { useRouter } from 'expo-router';
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';
import {
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import Auth from '../components/Auth';
import { supabase } from './lib/supabase';
export default function LoginScreen() {
const router = useRouter();
const handleLoginSuccess = () => {
router.replace('/Professor/ProfessorHome');
const handleLoginSuccess = async () => {
try {
// 1⃣ buscar utilizador autenticado
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (userError || !user) {
Alert.alert('Erro', 'Utilizador não autenticado');
return;
}
// 2⃣ buscar tipo (professor / aluno)
const { data, error } = await supabase
.from('profiles')
.select('tipo')
.eq('id', user.id)
.single();
if (error || !data) {
Alert.alert(
'Erro',
'Não foi possível obter o tipo de utilizador'
);
return;
}
// 3⃣ redirecionar conforme o tipo
if (data.tipo === 'professor') {
router.replace('/Professor/ProfessorHome');
} else if (data.tipo === 'aluno') {
router.replace('/Aluno/AlunoHome');
} else {
Alert.alert('Erro', 'Tipo de utilizador inválido');
}
} catch (err) {
Alert.alert('Erro', 'Erro inesperado no login');
}
};
return (
<KeyboardAvoidingView style={{ flex: 1, backgroundColor: '#f8f9fa' }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: '#f8f9fa' }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>📱 Estágios+</Text>
<Text style={styles.subtitle}>Escola Profissional de Vila do Conde</Text>
<Text style={styles.subtitle}>
Escola Profissional de Vila do Conde
</Text>
</View>
{/* Componente Auth */}
{/* COMPONENTE DE LOGIN */}
<Auth onLoginSuccess={handleLoginSuccess} />
</View>
</ScrollView>
@@ -28,9 +82,29 @@ export default function LoginScreen() {
}
const styles = StyleSheet.create({
scrollContainer: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 40 },
content: { flex: 1, justifyContent: 'center' },
header: { alignItems: 'center', marginBottom: 48 },
title: { fontSize: 32, fontWeight: '800', color: '#2d3436', marginBottom: 8 },
subtitle: { fontSize: 16, color: '#636e72', textAlign: 'center' },
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: 24,
paddingVertical: 40,
},
content: {
flex: 1,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: 48,
},
title: {
fontSize: 32,
fontWeight: '800',
color: '#2d3436',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#636e72',
textAlign: 'center',
},
});