criação da empresa
This commit is contained in:
@@ -56,7 +56,12 @@ const AlunoHome = memo(() => {
|
||||
const [selectedDate, setSelectedDate] = useState(hojeStr);
|
||||
const [estagioDetalhes, setEstagioDetalhes] = useState<any>(null);
|
||||
const [horariosEstagio, setHorariosEstagio] = useState<any[]>([]);
|
||||
const [presencas, setPresencas] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 🟢 NOVOS ESTADOS PARA REFLETIR A DECISÃO DO TUTOR
|
||||
const [presencasPendentes, setPresencasPendentes] = useState<Record<string, boolean>>({});
|
||||
const [presencasAprovadas, setPresencasAprovadas] = useState<Record<string, boolean>>({});
|
||||
const [presencasRejeitadas, setPresencasRejeitadas] = useState<Record<string, boolean>>({});
|
||||
|
||||
const [faltas, setFaltas] = useState<Record<string, boolean>>({});
|
||||
const [sumarios, setSumarios] = useState<Record<string, string>>({});
|
||||
const [urlsJustificacao, setUrlsJustificacao] = useState<Record<string, string>>({});
|
||||
@@ -137,13 +142,24 @@ const AlunoHome = memo(() => {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const p: any = {}, f: any = {}, s: any = {}, u: any = {};
|
||||
// 🟢 SEPARAMOS AS PRESENÇAS PELO ESTADO DO TUTOR
|
||||
const pPendente: any = {}, pAprovada: any = {}, pRejeitada: any = {};
|
||||
const f: any = {}, s: any = {}, u: any = {};
|
||||
|
||||
let countJustificadas = 0;
|
||||
let countInjustificadas = 0;
|
||||
let countPresencasAprovadas = 0;
|
||||
|
||||
data?.forEach(item => {
|
||||
if (item.estado === 'presente') {
|
||||
p[item.data] = true;
|
||||
// Guarda o estado para o calendário
|
||||
if (item.estado_tutor === 'pendente') pPendente[item.data] = true;
|
||||
else if (item.estado_tutor === 'aprovado') {
|
||||
pAprovada[item.data] = true;
|
||||
countPresencasAprovadas++; // Só soma se o tutor aprovou
|
||||
}
|
||||
else if (item.estado_tutor === 'rejeitado') pRejeitada[item.data] = true;
|
||||
|
||||
s[item.data] = item.sumario || '';
|
||||
} else {
|
||||
f[item.data] = true;
|
||||
@@ -153,10 +169,16 @@ const AlunoHome = memo(() => {
|
||||
}
|
||||
});
|
||||
|
||||
setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u);
|
||||
setStatsFaltas({ justificadas: countJustificadas, injustificadas: countInjustificadas, totalPresencas: Object.keys(p).length });
|
||||
setPresencasPendentes(pPendente);
|
||||
setPresencasAprovadas(pAprovada);
|
||||
setPresencasRejeitadas(pRejeitada);
|
||||
setFaltas(f);
|
||||
setSumarios(s);
|
||||
setUrlsJustificacao(u);
|
||||
setStatsFaltas({ justificadas: countJustificadas, injustificadas: countInjustificadas, totalPresencas: countPresencasAprovadas });
|
||||
} else {
|
||||
setPresencas({}); setFaltas({}); setSumarios({}); setUrlsJustificacao({});
|
||||
setPresencasPendentes({}); setPresencasAprovadas({}); setPresencasRejeitadas({});
|
||||
setFaltas({}); setSumarios({}); setUrlsJustificacao({});
|
||||
setStatsFaltas({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
|
||||
}
|
||||
|
||||
@@ -219,6 +241,11 @@ const AlunoHome = memo(() => {
|
||||
};
|
||||
}, [selectedDate, estagioDetalhes, hojeStr, feriadosMap, statusEstagio]);
|
||||
|
||||
// 🟢 FUNÇÃO AUXILIAR PARA SABER SE O DIA JÁ TEM REGISTO (PARA DESATIVAR BOTÕES)
|
||||
const isDiaMarcado = () => {
|
||||
return !!presencasAprovadas[selectedDate] || !!presencasPendentes[selectedDate] || !!presencasRejeitadas[selectedDate] || !!faltas[selectedDate];
|
||||
};
|
||||
|
||||
const handlePresencaClick = async () => {
|
||||
if (!infoData.temEstagio) return showAlert("Aguarde pela configuração do estágio.", "error");
|
||||
if (!infoData.estagioAtivo) return showAlert("O estágio não está ativo neste momento.", "error");
|
||||
@@ -240,9 +267,14 @@ const AlunoHome = memo(() => {
|
||||
const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
await supabase.from('presencas').upsert({
|
||||
aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude
|
||||
aluno_id: user?.id,
|
||||
data: selectedDate,
|
||||
estado: 'presente',
|
||||
lat: loc.coords.latitude,
|
||||
lng: loc.coords.longitude,
|
||||
estado_tutor: 'pendente' // 🟢 NOVO REGISTO ENTRA COMO PENDENTE
|
||||
});
|
||||
showAlert("Presença marcada!", "success");
|
||||
showAlert("Presença marcada! A aguardar aprovação.", "success");
|
||||
} catch (e: any) { showAlert(e.message, "error"); }
|
||||
finally { setIsLocating(false); }
|
||||
};
|
||||
@@ -299,6 +331,20 @@ const AlunoHome = memo(() => {
|
||||
const horasConcluidas = estagioDetalhes?.horas_concluidas || 0;
|
||||
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
|
||||
|
||||
// 🟢 FUNÇÃO PARA MOSTRAR AVISO DE ESTADO DO DIA
|
||||
const renderAvisoEstadoDia = () => {
|
||||
if (presencasAprovadas[selectedDate]) {
|
||||
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.verde }}>✅ Horas validadas pela empresa</Text>;
|
||||
}
|
||||
if (presencasPendentes[selectedDate]) {
|
||||
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.laranja }}>⏳ A aguardar validação do tutor</Text>;
|
||||
}
|
||||
if (presencasRejeitadas[selectedDate]) {
|
||||
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.vermelho }}>❌ O tutor rejeitou este registo. Corrige o sumário.</Text>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
@@ -456,7 +502,7 @@ const AlunoHome = memo(() => {
|
||||
|
||||
<View style={styles.dashGrid}>
|
||||
<View style={styles.dashGridItem}>
|
||||
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>PRESENÇAS</Text>
|
||||
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>APROVADAS</Text>
|
||||
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{statsFaltas.totalPresencas}</Text>
|
||||
</View>
|
||||
<View style={styles.dashDividerVertical} />
|
||||
@@ -552,16 +598,16 @@ const AlunoHome = memo(() => {
|
||||
|
||||
<View style={styles.botoesLinha}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || isDiaMarcado()) && styles.disabled]}
|
||||
onPress={handlePresencaClick}
|
||||
disabled={!infoData.podeMarcar || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
|
||||
disabled={!infoData.podeMarcar || isDiaMarcado() || isLocating}
|
||||
>
|
||||
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, { backgroundColor: themeStyles.vermelho }, (!infoData.valida || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
style={[styles.btn, { backgroundColor: themeStyles.vermelho }, (!infoData.valida || isDiaMarcado()) && styles.disabled]}
|
||||
onPress={handleFalta}
|
||||
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
|
||||
disabled={!infoData.valida || isDiaMarcado()}
|
||||
>
|
||||
<Text style={styles.txtBtn}>Marcar Falta</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -576,7 +622,10 @@ const AlunoHome = memo(() => {
|
||||
}}
|
||||
markedDates={{
|
||||
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
|
||||
...Object.keys(presencas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.verde } }), {}),
|
||||
// 🟢 AS CORES AGORA REFLETEM O ESTADO DO TUTOR
|
||||
...Object.keys(presencasAprovadas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.verde } }), {}),
|
||||
...Object.keys(presencasPendentes).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.laranja } }), {}),
|
||||
...Object.keys(presencasRejeitadas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
|
||||
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
|
||||
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
|
||||
}}
|
||||
@@ -588,7 +637,11 @@ const AlunoHome = memo(() => {
|
||||
<Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.textoSecundario }}>🎉 {infoData.nomeFeriado}</Text>
|
||||
)}
|
||||
|
||||
{presencas[selectedDate] && (
|
||||
{/* 🟢 MOSTRA O AVISO DO ESTADO DO TUTOR */}
|
||||
{renderAvisoEstadoDia()}
|
||||
|
||||
{/* SE O ALUNO ESTIVER PRESENTE (Pendente ou Aprovado), MOSTRA O SUMÁRIO */}
|
||||
{(presencasPendentes[selectedDate] || presencasAprovadas[selectedDate] || presencasRejeitadas[selectedDate]) && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.rowTitle}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
|
||||
@@ -639,38 +692,11 @@ const styles = StyleSheet.create({
|
||||
topIcons: { flexDirection: 'row', alignItems: 'center' },
|
||||
title: { fontSize: 26, fontWeight: '900' },
|
||||
|
||||
// Estilos das Tabs (Separadores) Modernos
|
||||
quickActionsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 20,
|
||||
borderRadius: 20,
|
||||
padding: 6, // Cria aquele espaço interior para parecer uma pílula
|
||||
},
|
||||
quickActionBtn: {
|
||||
flex: 1,
|
||||
flexDirection: 'row', // Coloca o ícone e o texto lado a lado
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 16,
|
||||
gap: 6, // Espaço entre o ícone e o texto
|
||||
elevation: 0, // Garante que não há sombra base que manche o ecrã
|
||||
shadowOpacity: 0,
|
||||
borderWidth: 0,
|
||||
},
|
||||
quickActionBtnActive: {
|
||||
// 🔥 Removemos as sombras feias daqui! O contraste faz-se pela cor de fundo limpa.
|
||||
elevation: 0,
|
||||
shadowOpacity: 0,
|
||||
borderWidth: 0,
|
||||
},
|
||||
quickActionText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '800'
|
||||
},
|
||||
quickActionsContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20, borderRadius: 20, padding: 6 },
|
||||
quickActionBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, borderRadius: 16, gap: 6, elevation: 0, shadowOpacity: 0, borderWidth: 0 },
|
||||
quickActionBtnActive: { elevation: 0, shadowOpacity: 0, borderWidth: 0 },
|
||||
quickActionText: { fontSize: 13, fontWeight: '800' },
|
||||
|
||||
// Estilos do Cartão que muda
|
||||
dashboardCard: { padding: 18, borderRadius: 20, borderWidth: 1, borderLeftWidth: 5, marginBottom: 20, elevation: 2, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||
dashHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 },
|
||||
dashEmpresa: { fontSize: 16, fontWeight: '800' },
|
||||
@@ -685,7 +711,6 @@ const styles = StyleSheet.create({
|
||||
dashDividerHorizontal: { height: 1, marginVertical: 12, opacity: 0.6 },
|
||||
dashDividerVertical: { width: 1, height: 30, backgroundColor: '#E2E8F0', opacity: 0.6 },
|
||||
|
||||
// Estilos para a Tab "Info"
|
||||
infoRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
||||
infoLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 },
|
||||
infoValue: { fontSize: 15, fontWeight: '700' },
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
@@ -24,14 +26,13 @@ export default function PerfilAluno() {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [perfil, setPerfil] = useState<any>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// --- ESTADOS PARA O ALERTA MODERNO ---
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
const fadeAnim = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
@@ -47,7 +48,6 @@ export default function PerfilAluno() {
|
||||
verde: '#10B981',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// --- FUNÇÃO DE MOSTRAR O ALERTA ---
|
||||
const showAlert = useCallback((msg: string, type: 'success' | 'error') => {
|
||||
setAlertConfig({ msg, type });
|
||||
Animated.sequence([
|
||||
@@ -57,7 +57,6 @@ export default function PerfilAluno() {
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, [alertOpacity]);
|
||||
|
||||
// --- FUNÇÕES DE DATA ---
|
||||
const formatarDataParaUI = (dataDB: string) => {
|
||||
if (!dataDB) return '';
|
||||
const parts = dataDB.split('-');
|
||||
@@ -105,26 +104,22 @@ export default function PerfilAluno() {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const buscarDados = async () => {
|
||||
const buscarDados = async (isRefreshing = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!isRefreshing) setLoading(true);
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
// 🟢 CORREÇÃO: Vamos buscar as DUAS tabelas sempre usando o ID do utilizador (infalível)
|
||||
const { data: pData } = await supabase.from('profiles').select('*').eq('id', user.id).single();
|
||||
|
||||
let aData = {};
|
||||
if (pData?.n_escola) {
|
||||
const { data: alunoRes } = await supabase.from('alunos').select('*').eq('n_escola', pData.n_escola).single();
|
||||
if (alunoRes) aData = alunoRes;
|
||||
}
|
||||
const { data: aData } = await supabase.from('alunos').select('*').eq('id', user.id).maybeSingle();
|
||||
|
||||
const dataFormatadaUI = formatarDataParaUI(pData?.data_nascimento);
|
||||
const idadeCalculada = dataFormatadaUI ? calcularIdade(dataFormatadaUI) : pData?.idade;
|
||||
|
||||
setPerfil({
|
||||
...aData,
|
||||
...pData,
|
||||
...aData, // A tabela de alunos (onde está o curso) sobrepõe e preenche o que falta
|
||||
email: user.email,
|
||||
data_nascimento: dataFormatadaUI,
|
||||
idade: idadeCalculada ?? 'N/A'
|
||||
@@ -135,10 +130,20 @@ export default function PerfilAluno() {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { buscarDados(); }, []);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
buscarDados();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
buscarDados(true);
|
||||
}, []);
|
||||
|
||||
const salvarDados = async () => {
|
||||
try {
|
||||
@@ -148,24 +153,27 @@ export default function PerfilAluno() {
|
||||
|
||||
const dataProntaParaDB = formatarDataParaDB(perfil.data_nascimento);
|
||||
|
||||
const { error, data } = await supabase.from('profiles').update({
|
||||
// 1. Atualizar a tabela de Perfis Pessoais
|
||||
const { error: errorProfile } = await supabase.from('profiles').update({
|
||||
nome: perfil.nome,
|
||||
residencia: perfil.residencia,
|
||||
data_nascimento: dataProntaParaDB,
|
||||
idade: perfil.idade !== 'N/A' ? Number(perfil.idade) : null,
|
||||
telefone: perfil.telefone
|
||||
}).eq('id', user.id).select();
|
||||
}).eq('id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error("Erro nas permissões. Confirma as tuas políticas no Supabase.");
|
||||
}
|
||||
if (errorProfile) throw errorProfile;
|
||||
|
||||
// 2. Atualizar a tabela de Alunos (só o nome, para manter tudo sincronizado)
|
||||
// Não lançamos erro aqui para não bloquear o utilizador se ele não tiver permissões RLS no momento
|
||||
await supabase.from('alunos').update({ nome: perfil.nome }).eq('id', user.id);
|
||||
|
||||
setIsEditing(false);
|
||||
// 🔥 O NOSSO NOVO ALERTA MODERNO EM ACÇÃO 🔥
|
||||
showAlert("Perfil guardado com sucesso!", "success");
|
||||
await buscarDados();
|
||||
|
||||
// OBRIGA O ECRÃ A ATUALIZAR OS DADOS VISUAIS FRESQUINHOS
|
||||
await buscarDados();
|
||||
|
||||
} catch (e: any) {
|
||||
showAlert(e.message || "Erro ao guardar alterações.", "error");
|
||||
} finally {
|
||||
@@ -173,13 +181,12 @@ export default function PerfilAluno() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <View style={[styles.centered, { backgroundColor: cores.fundo }]}><ActivityIndicator size="large" color={cores.azul} /></View>;
|
||||
if (loading && !refreshing) return <View style={[styles.centered, { backgroundColor: cores.fundo }]}><ActivityIndicator size="large" color={cores.azul} /></View>;
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={{ flex: 1, backgroundColor: cores.fundo }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
{/* 🟢 COMPONENTE DE ALERTA FLUTUANTE 🟢 */}
|
||||
{alertConfig && (
|
||||
<Animated.View style={[
|
||||
styles.modernAlert,
|
||||
@@ -210,12 +217,16 @@ export default function PerfilAluno() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[cores.azul]} tintColor={cores.azul} />}
|
||||
>
|
||||
<Animated.View style={{ opacity: fadeAnim }}>
|
||||
|
||||
<View style={styles.avatarSection}>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarInitial}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
<Text style={styles.avatarInitial}>{perfil?.nome?.charAt(0)?.toUpperCase() || '?'}</Text>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.profileName, { color: cores.texto, borderBottomWidth: isEditing ? 1 : 0, borderBottomColor: cores.borda }]}
|
||||
@@ -234,7 +245,7 @@ export default function PerfilAluno() {
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card }]}>
|
||||
<View style={styles.inputRow}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<PerfilInput label="Nº Escola" icon="id-card-outline" value={perfil?.n_escola?.toString()} editable={false} cores={cores} />
|
||||
<PerfilInput label="Nº Escola" icon="id-card-outline" value={perfil?.n_escola?.toString() || 'N/A'} editable={false} cores={cores} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<PerfilInput label="Ano" icon="calendar-outline" value={perfil?.ano ? `${perfil.ano}º Ano` : 'N/A'} editable={false} cores={cores} />
|
||||
@@ -313,31 +324,8 @@ const PerfilInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
// ESTILO DO ALERTA MODERNO
|
||||
modernAlert: {
|
||||
position: 'absolute',
|
||||
left: 20,
|
||||
right: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 20,
|
||||
zIndex: 9999,
|
||||
elevation: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 5 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 10
|
||||
},
|
||||
modernAlertText: {
|
||||
color: '#fff',
|
||||
fontWeight: '800',
|
||||
fontSize: 14,
|
||||
marginLeft: 10,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
modernAlert: { position: 'absolute', left: 20, right: 20, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, zIndex: 9999, elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 5 }, shadowOpacity: 0.2, shadowRadius: 10 },
|
||||
modernAlertText: { color: '#fff', fontWeight: '800', fontSize: 14, marginLeft: 10, flex: 1 },
|
||||
headerContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 19, fontWeight: '900' },
|
||||
roundBtn: { width: 45, height: 45, borderRadius: 14, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
|
||||
|
||||
274
app/Empresas/EmpresaHome.tsx
Normal file
274
app/Empresas/EmpresaHome.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function EmpresaHome() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const [pendentes, setPendentes] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [empresaNome, setEmpresaNome] = useState('');
|
||||
|
||||
const themeStyles = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: '#2390a6',
|
||||
laranja: '#dd8707',
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
|
||||
inputFundo: isDarkMode ? '#252525' : '#F1F5F9',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
// 1. Identificar quem é a empresa que tem o login feito
|
||||
const { data: empresa } = await supabase
|
||||
.from('empresas')
|
||||
.select('id, nome')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (!empresa) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
setEmpresaNome(empresa.nome);
|
||||
|
||||
// 2. Buscar todos os estágios ligados a esta empresa
|
||||
const { data: estagios } = await supabase
|
||||
.from('estagios')
|
||||
.select('aluno_id')
|
||||
.eq('empresa_id', empresa.id);
|
||||
|
||||
if (!estagios || estagios.length === 0) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const alunoIds = estagios.map(e => e.aluno_id);
|
||||
|
||||
// 3. Buscar os nomes dos alunos (para o tutor saber quem está a avaliar)
|
||||
const { data: alunos } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome')
|
||||
.in('id', alunoIds);
|
||||
|
||||
const mapaAlunos: Record<string, string> = {};
|
||||
alunos?.forEach(a => { mapaAlunos[a.id] = a.nome; });
|
||||
|
||||
// 4. Buscar apenas as presenças que estão PENDENTES para estes alunos
|
||||
const { data: presencas } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.in('aluno_id', alunoIds)
|
||||
.eq('estado', 'presente')
|
||||
.eq('estado_tutor', 'pendente')
|
||||
.order('data', { ascending: false });
|
||||
|
||||
const listaFormatada = presencas?.map(p => ({
|
||||
...p,
|
||||
aluno_nome: mapaAlunos[p.aluno_id] || 'Aluno Desconhecido'
|
||||
})) || [];
|
||||
|
||||
setPendentes(listaFormatada);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Alert.alert("Erro", "Falha ao carregar as validações pendentes.");
|
||||
} finally {
|
||||
if (!isManualRefresh) setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Atualiza sempre que o ecrã ganha foco
|
||||
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchValidaçõesPendentes(true);
|
||||
}, []);
|
||||
|
||||
// 🟢 FUNÇÃO PARA APROVAR OU REJEITAR
|
||||
const lidarComPresenca = async (aluno_id: string, data: string, decisao: 'aprovado' | 'rejeitado') => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('presencas')
|
||||
.update({ estado_tutor: decisao })
|
||||
.match({ aluno_id, data }); // Dá match exato ao aluno e àquele dia
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Avisa visualmente do sucesso e limpa aquele cartão da lista
|
||||
if (decisao === 'aprovado') {
|
||||
Alert.alert("✅ Validado", "Horas e sumário aprovados com sucesso!");
|
||||
} else {
|
||||
Alert.alert("❌ Rejeitado", "O registo foi devolvido ao aluno para correção.");
|
||||
}
|
||||
|
||||
// Atualiza a lista removendo o que acabou de ser processado
|
||||
setPendentes(prev => prev.filter(p => !(p.aluno_id === aluno_id && p.data === data)));
|
||||
|
||||
} catch (e: any) {
|
||||
Alert.alert("Erro ao validar", e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Formatar data (AAAA-MM-DD -> DD/MM/AAAA)
|
||||
const formatarData = (dataStr: string) => {
|
||||
if (!dataStr) return '';
|
||||
const parts = dataStr.split('-');
|
||||
if (parts.length !== 3) return dataStr;
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.topBar}>
|
||||
<View>
|
||||
<Text style={[styles.greeting, { color: themeStyles.textoSecundario }]}>Painel da Entidade</Text>
|
||||
<Text style={[styles.title, { color: themeStyles.texto }]}>{empresaNome || 'A carregar...'}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.logoutBtn, { borderColor: themeStyles.borda }]} onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}>
|
||||
<Ionicons name="log-out-outline" size={24} color={themeStyles.vermelho} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
|
||||
<View style={[styles.badge, { backgroundColor: themeStyles.laranja + '20' }]}>
|
||||
<Text style={[styles.badgeText, { color: themeStyles.laranja }]}>{pendentes.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading && !refreshing ? (
|
||||
<View style={styles.centerBox}>
|
||||
<ActivityIndicator size="large" color={themeStyles.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />}
|
||||
>
|
||||
{pendentes.length === 0 ? (
|
||||
<View style={[styles.emptyBox, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}>
|
||||
<Ionicons name="checkmark-done-circle" size={60} color={themeStyles.verde} style={{ marginBottom: 15 }} />
|
||||
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Tudo em dia!</Text>
|
||||
<Text style={[styles.emptyDesc, { color: themeStyles.textoSecundario }]}>
|
||||
Não tens sumários ou presenças de alunos a aguardar a tua validação neste momento.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
pendentes.map((item, index) => (
|
||||
<View key={index} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>
|
||||
<Ionicons name="person" size={14} color={themeStyles.textoSecundario} /> {item.aluno_nome}
|
||||
</Text>
|
||||
<Text style={[styles.dataText, { color: themeStyles.azul }]}>
|
||||
<Ionicons name="calendar-outline" size={12} /> {formatarData(item.data)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.statusTag, { backgroundColor: themeStyles.laranja + '20' }]}>
|
||||
<Text style={[styles.statusTagText, { color: themeStyles.laranja }]}>POR VALIDAR</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.sumarioBox, { backgroundColor: themeStyles.inputFundo }]}>
|
||||
<Text style={[styles.sumarioLabel, { color: themeStyles.textoSecundario }]}>Sumário Submetido:</Text>
|
||||
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
|
||||
{item.sumario ? item.sumario : "O aluno não escreveu sumário para este dia."}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]}
|
||||
onPress={() => lidarComPresenca(item.aluno_id, item.data, 'rejeitado')}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={themeStyles.vermelho} />
|
||||
<Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.verde }]}
|
||||
onPress={() => lidarComPresenca(item.aluno_id, item.data, 'aprovado')}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color="#fff" />
|
||||
<Text style={[styles.btnActionText, { color: '#fff' }]}>Aprovar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 10 },
|
||||
greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
title: { fontSize: 24, fontWeight: '900', marginTop: 2 },
|
||||
logoutBtn: { width: 45, height: 45, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
headerTitleContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 15, gap: 10 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '800' },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 },
|
||||
badgeText: { fontSize: 12, fontWeight: '900' },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
|
||||
emptyBox: { alignItems: 'center', padding: 40, borderRadius: 24, borderWidth: 1, borderStyle: 'dashed', marginTop: 30 },
|
||||
emptyTitle: { fontSize: 20, fontWeight: '900', marginBottom: 8 },
|
||||
emptyDesc: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '500' },
|
||||
|
||||
card: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 15 },
|
||||
alunoName: { fontSize: 17, fontWeight: '900', marginBottom: 4 },
|
||||
dataText: { fontSize: 13, fontWeight: '800' },
|
||||
statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 },
|
||||
statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
|
||||
|
||||
sumarioBox: { padding: 15, borderRadius: 16, marginBottom: 15 },
|
||||
sumarioLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6 },
|
||||
sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 20 },
|
||||
|
||||
actionRow: { flexDirection: 'row', gap: 12 },
|
||||
btnAction: { flex: 1, flexDirection: 'row', height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center', gap: 6 },
|
||||
btnActionText: { fontSize: 14, fontWeight: '800' }
|
||||
});
|
||||
@@ -18,11 +18,39 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
// Função para calcular idade automaticamente
|
||||
const calcularIdade = (data: string): string => {
|
||||
if (!data || data.length < 10) return '';
|
||||
// --- FUNÇÕES DE DATA ---
|
||||
const formatarDataParaDB = (dataUI: string) => {
|
||||
if (!dataUI || dataUI.length !== 10) return null;
|
||||
const parts = dataUI.split('-');
|
||||
if (parts.length !== 3) return null;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const aplicarMascaraData = (text: string) => {
|
||||
const cleaned = text.replace(/\D/g, '');
|
||||
let formatted = cleaned;
|
||||
if (cleaned.length > 2 && cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`;
|
||||
} else if (cleaned.length > 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`;
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const calcularIdade = (dataPT: string): string => {
|
||||
if (!dataPT || dataPT.length !== 10) return '';
|
||||
const parts = dataPT.split('-');
|
||||
const dia = parseInt(parts[0], 10);
|
||||
const mes = parseInt(parts[1], 10);
|
||||
const ano = parseInt(parts[2], 10);
|
||||
|
||||
if (ano < 1950 || ano > new Date().getFullYear()) return '';
|
||||
|
||||
const hoje = new Date();
|
||||
const nascimento = new Date(data);
|
||||
const nascimento = new Date(ano, mes - 1, dia);
|
||||
|
||||
if (nascimento.getDate() !== dia) return '';
|
||||
|
||||
let idade = hoje.getFullYear() - nascimento.getFullYear();
|
||||
const m = hoje.getMonth() - nascimento.getMonth();
|
||||
if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) idade--;
|
||||
@@ -39,27 +67,27 @@ const CriarAluno = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [tipo, setTipo] = useState<'aluno' | 'professor' | 'empresa'>('aluno');
|
||||
|
||||
// ESTADOS DE PERFIL (Comuns)
|
||||
// ESTADOS DE PERFIL (Utilizador / Tutor)
|
||||
const [nome, setNome] = useState('');
|
||||
const [residencia, setResidencia] = useState('');
|
||||
const [telefone, setTelefone] = useState('');
|
||||
const [dataNascimento, setDataNascimento] = useState(''); // Formato AAAA-MM-DD
|
||||
const [dataNascimento, setDataNascimento] = useState(''); // Formato DD-MM-AAAA
|
||||
const [idade, setIdade] = useState('');
|
||||
|
||||
// ESTADOS ESPECÍFICOS
|
||||
// ESTADOS ESPECÍFICOS (Aluno / Professor)
|
||||
const [ano, setAno] = useState('');
|
||||
const [nEscola, setNEscola] = useState('');
|
||||
const [curso, setCurso] = useState('');
|
||||
const [setor, setSetor] = useState('');
|
||||
|
||||
// CAMPOS PARA EMPRESA (Tutor)
|
||||
const [tutorNome, setTutorNome] = useState('');
|
||||
const [tutorTelefone, setTutorTelefone] = useState('');
|
||||
// CAMPOS PARA EMPRESA
|
||||
const [nomeEmpresa, setNomeEmpresa] = useState('');
|
||||
const [nif, setNif] = useState('');
|
||||
const [setor, setSetor] = useState('');
|
||||
|
||||
// Atualiza idade sempre que a data de nascimento mudar
|
||||
useEffect(() => {
|
||||
const novaIdade = calcularIdade(dataNascimento);
|
||||
if (novaIdade) setIdade(novaIdade);
|
||||
else setIdade('');
|
||||
}, [dataNascimento]);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
@@ -81,12 +109,14 @@ const CriarAluno = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tipo === 'empresa' && (!nomeEmpresa || !nif)) {
|
||||
Alert.alert("Atenção", "Nome da Entidade e NIF são obrigatórios para Empresas.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Criar Auth User
|
||||
// IMPORTANTE: Se o "Confirm Email" estiver ativo no Supabase,
|
||||
// o signUp não inicia sessão automaticamente.
|
||||
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||
email: emailLimpo,
|
||||
password,
|
||||
@@ -105,25 +135,25 @@ const CriarAluno = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Inserir em PROFILES
|
||||
const dataFormatadaDB = formatarDataParaDB(dataNascimento);
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.insert([{
|
||||
id: user.id,
|
||||
nome,
|
||||
nome, // É o nome da pessoa (Aluno, Prof, ou Tutor)
|
||||
email: emailLimpo,
|
||||
residencia,
|
||||
telefone,
|
||||
idade: idade ? parseInt(idade) : null,
|
||||
data_nascimento: dataNascimento || null,
|
||||
data_nascimento: dataFormatadaDB,
|
||||
tipo,
|
||||
n_escola: tipo !== 'professor' ? nEscola : null,
|
||||
curso: tipo === 'aluno' ? curso : (tipo === 'professor' ? curso : setor)
|
||||
n_escola: tipo === 'aluno' ? nEscola : null, // Apenas para aluno
|
||||
curso: tipo === 'aluno' || tipo === 'professor' ? curso : null
|
||||
}]);
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
// 3. Inserir na tabela específica de ALUNOS
|
||||
if (tipo === 'aluno') {
|
||||
const { error: alunoError } = await supabase
|
||||
.from('alunos')
|
||||
@@ -137,18 +167,18 @@ const CriarAluno = () => {
|
||||
if (alunoError) throw alunoError;
|
||||
}
|
||||
|
||||
// 4. Se for EMPRESA
|
||||
if (tipo === 'empresa') {
|
||||
const { error: empresaError } = await supabase
|
||||
.from('empresas')
|
||||
.insert([{
|
||||
nome,
|
||||
nif: nEscola,
|
||||
setor,
|
||||
tutor_nome: tutorNome,
|
||||
tutor_telefone: tutorTelefone,
|
||||
nome: nomeEmpresa,
|
||||
nif: nif,
|
||||
setor: setor,
|
||||
tutor_nome: nome, // O nome do perfil é o nome do tutor
|
||||
tutor_telefone: telefone, // O telefone do perfil é o contacto do tutor
|
||||
user_id: user.id
|
||||
}]);
|
||||
if (empresaError) throw empresaError;
|
||||
}
|
||||
|
||||
Alert.alert("Sucesso", "Novo registo concluído com sucesso!");
|
||||
@@ -156,7 +186,6 @@ const CriarAluno = () => {
|
||||
|
||||
} catch (err: any) {
|
||||
Alert.alert("Erro ao criar conta", err.message);
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -176,7 +205,6 @@ const CriarAluno = () => {
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* SELETOR DE TIPO */}
|
||||
<View style={styles.selectorContainer}>
|
||||
{(['aluno', 'professor', 'empresa'] as const).map((item) => (
|
||||
<TouchableOpacity
|
||||
@@ -195,23 +223,23 @@ const CriarAluno = () => {
|
||||
<SectionHeader title="Credenciais de Acesso" cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={email} onChangeText={setEmail} placeholder="Email Institucional" autoCapitalize="none" placeholderTextColor={cores.placeholder}
|
||||
value={email} onChangeText={setEmail} placeholder="Email Institucional / Comercial" autoCapitalize="none" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={password} onChangeText={setPassword} secureTextEntry placeholder="Password (mín. 6 caracteres)" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
<SectionHeader title="Dados Pessoais" cores={cores} />
|
||||
<SectionHeader title={tipo === 'empresa' ? "Dados do Tutor Responsável" : "Dados Pessoais"} cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? "Nome da Entidade" : "Nome Completo"} placeholderTextColor={cores.placeholder}
|
||||
value={nome} onChangeText={setNome} placeholder={tipo === 'empresa' ? "Nome do Tutor" : "Nome Completo"} placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 2, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={dataNascimento} onChangeText={setDataNascimento} placeholder="Nascimento (AAAA-MM-DD)" placeholderTextColor={cores.placeholder}
|
||||
value={dataNascimento} onChangeText={(t) => setDataNascimento(aplicarMascaraData(t))} placeholder="Nascimento (DD-MM-AAAA)" maxLength={10} keyboardType="numeric" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<View style={[styles.input, { flex: 1, backgroundColor: cores.card, borderColor: cores.borda, justifyContent: 'center', opacity: 0.7 }]}>
|
||||
<Text style={{ color: cores.texto, textAlign: 'center' }}>{idade ? `${idade} anos` : 'Idade'}</Text>
|
||||
@@ -224,10 +252,9 @@ const CriarAluno = () => {
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={residencia} onChangeText={setResidencia} placeholder="Morada Completa" placeholderTextColor={cores.placeholder}
|
||||
value={residencia} onChangeText={setResidencia} placeholder={tipo === 'empresa' ? "Morada do Tutor" : "Morada Completa"} placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
|
||||
{/* CAMPOS DINÂMICOS */}
|
||||
{tipo === 'aluno' && (
|
||||
<>
|
||||
<SectionHeader title="Percurso Escolar" cores={cores} />
|
||||
@@ -260,25 +287,21 @@ const CriarAluno = () => {
|
||||
|
||||
{tipo === 'empresa' && (
|
||||
<>
|
||||
<SectionHeader title="Dados da Entidade & Tutor" cores={cores} />
|
||||
<SectionHeader title="Dados da Entidade (Empresa)" cores={cores} />
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nomeEmpresa} onChangeText={setNomeEmpresa} placeholder="Nome Oficial da Empresa" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<View style={{ flexDirection: 'row', gap: 10 }}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={nEscola} onChangeText={setNEscola} keyboardType="numeric" placeholder="NIF" placeholderTextColor={cores.placeholder}
|
||||
value={nif} onChangeText={setNif} keyboardType="numeric" placeholder="NIF" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={setor} onChangeText={setSetor} placeholder="Setor Atividade" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={tutorNome} onChangeText={setTutorNome} placeholder="Nome do Tutor Responsável" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.card, color: cores.texto, borderColor: cores.borda }]}
|
||||
value={tutorTelefone} onChangeText={setTutorTelefone} keyboardType="phone-pad" placeholder="Contacto do Tutor" placeholderTextColor={cores.placeholder}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -19,7 +19,21 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
|
||||
// --- UTILITÁRIOS ---
|
||||
// --- UTILITÁRIOS DE DATA ---
|
||||
const formatarDataParaUI = (dataDB: string) => {
|
||||
if (!dataDB) return '';
|
||||
const parts = dataDB.split('-');
|
||||
if (parts.length !== 3) return dataDB;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const formatarDataParaDB = (dataUI: string) => {
|
||||
if (!dataUI || dataUI.length !== 10) return null;
|
||||
const parts = dataUI.split('-');
|
||||
if (parts.length !== 3) return null;
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
};
|
||||
|
||||
const calcularIdade = (dataNascimento: string) => {
|
||||
if (!dataNascimento) return null;
|
||||
const hoje = new Date();
|
||||
@@ -32,6 +46,17 @@ const calcularIdade = (dataNascimento: string) => {
|
||||
return idade;
|
||||
};
|
||||
|
||||
const aplicarMascaraData = (text: string) => {
|
||||
const cleaned = text.replace(/\D/g, '');
|
||||
let formatted = cleaned;
|
||||
if (cleaned.length > 2 && cleaned.length <= 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2)}`;
|
||||
} else if (cleaned.length > 4) {
|
||||
formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 8)}`;
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// --- TIPAGENS ---
|
||||
interface AlunoEditForm {
|
||||
nome: string;
|
||||
@@ -56,13 +81,7 @@ const DetalhesAlunos = memo(() => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [editForm, setEditForm] = useState<AlunoEditForm>({
|
||||
nome: '',
|
||||
n_escola: '',
|
||||
turma_curso: '',
|
||||
telefone: '',
|
||||
residencia: '',
|
||||
data_nascimento: '',
|
||||
email: ''
|
||||
nome: '', n_escola: '', turma_curso: '', telefone: '', residencia: '', data_nascimento: '', email: ''
|
||||
});
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
@@ -118,7 +137,7 @@ const DetalhesAlunos = memo(() => {
|
||||
turma_curso: alunoData.turma_curso || '',
|
||||
telefone: perfilData?.telefone || '',
|
||||
residencia: perfilData?.residencia || '',
|
||||
data_nascimento: perfilData?.data_nascimento || '',
|
||||
data_nascimento: formatarDataParaUI(perfilData?.data_nascimento) || '',
|
||||
email: perfilData?.email || ''
|
||||
});
|
||||
|
||||
@@ -133,26 +152,36 @@ const DetalhesAlunos = memo(() => {
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const { error: err1 } = await supabase.from('alunos').update({
|
||||
nome: editForm.nome,
|
||||
n_escola: editForm.n_escola,
|
||||
turma_curso: editForm.turma_curso
|
||||
}).eq('id', alunoId);
|
||||
|
||||
const dataFormatadaDB = formatarDataParaDB(editForm.data_nascimento);
|
||||
const novaIdade = calcularIdade(dataFormatadaDB || '');
|
||||
|
||||
const { error: err2 } = await supabase.from('profiles').update({
|
||||
// Não enviamos o 'n_escola' para garantir que os dados institucionais não são tocados
|
||||
const { data: d1, error: err1 } = await supabase.from('alunos').update({
|
||||
nome: editForm.nome,
|
||||
turma_curso: editForm.turma_curso
|
||||
}).eq('id', alunoId).select();
|
||||
|
||||
// Não enviamos o 'email' pelo mesmo motivo
|
||||
const { data: d2, error: err2 } = await supabase.from('profiles').update({
|
||||
telefone: editForm.telefone,
|
||||
residencia: editForm.residencia,
|
||||
data_nascimento: editForm.data_nascimento,
|
||||
email: editForm.email
|
||||
}).eq('id', alunoId);
|
||||
data_nascimento: dataFormatadaDB,
|
||||
idade: novaIdade
|
||||
}).eq('id', alunoId).select();
|
||||
|
||||
if (err1 || err2) throw new Error("Erro na gravação dos dados");
|
||||
if (err1) throw err1;
|
||||
if (err2) throw err2;
|
||||
|
||||
Alert.alert("Sucesso", "Dados atualizados!");
|
||||
if (!d1 || d1.length === 0 || !d2 || d2.length === 0) {
|
||||
throw new Error("Permissão Negada: O professor não tem direitos (RLS) para editar este aluno no Supabase.");
|
||||
}
|
||||
|
||||
Alert.alert("Sucesso", "Dados atualizados com sucesso!");
|
||||
setModalVisible(false);
|
||||
fetchAluno();
|
||||
fetchAluno();
|
||||
} catch (err: any) {
|
||||
Alert.alert("Erro", "Não foi possível guardar. Verifica a ligação.");
|
||||
Alert.alert("Erro na Gravação", err.message || "Não foi possível guardar. Verifica a ligação.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -200,7 +229,7 @@ const DetalhesAlunos = memo(() => {
|
||||
<DetailRow
|
||||
icon="calendar-outline"
|
||||
label="Nascimento / Idade"
|
||||
value={aluno?.perfil?.data_nascimento ? `${aluno.perfil.data_nascimento} (${calcularIdade(aluno.perfil.data_nascimento)} anos)` : '-'}
|
||||
value={aluno?.perfil?.data_nascimento ? `${formatarDataParaUI(aluno.perfil.data_nascimento)} (${aluno.perfil.idade} anos)` : '-'}
|
||||
cores={cores}
|
||||
/>
|
||||
<DetailRow
|
||||
@@ -244,11 +273,11 @@ const DetalhesAlunos = memo(() => {
|
||||
<View style={styles.estagioFooter}>
|
||||
<View style={styles.periodoCol}>
|
||||
<Text style={styles.miniLabel}>INÍCIO</Text>
|
||||
<Text style={styles.footerVal}>{aluno?.estagio?.data_inicio || '-'}</Text>
|
||||
<Text style={styles.footerVal}>{formatarDataParaUI(aluno?.estagio?.data_inicio) || '-'}</Text>
|
||||
</View>
|
||||
<View style={[styles.periodoCol, {alignItems: 'flex-end'}]}>
|
||||
<Text style={styles.miniLabel}>FIM</Text>
|
||||
<Text style={styles.footerVal}>{aluno?.estagio?.data_fim || '-'}</Text>
|
||||
<Text style={styles.footerVal}>{formatarDataParaUI(aluno?.estagio?.data_fim) || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -272,11 +301,21 @@ const DetalhesAlunos = memo(() => {
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<EditInput label="Nome Completo" value={editForm.nome} onChange={(t: string) => setEditForm({...editForm, nome: t})} cores={cores} />
|
||||
<EditInput label="Nº Escola" value={editForm.n_escola} onChange={(t: string) => setEditForm({...editForm, n_escola: t})} cores={cores} keyboard="numeric" />
|
||||
|
||||
{/* CAMPOS INSTITUCIONAIS BLOQUEADOS (editable={false}) */}
|
||||
<EditInput label="Nº Escola (Institucional)" value={editForm.n_escola} cores={cores} editable={false} />
|
||||
<EditInput label="Email Institucional" value={editForm.email} cores={cores} editable={false} />
|
||||
|
||||
<EditInput label="Turma/Curso" value={editForm.turma_curso} onChange={(t: string) => setEditForm({...editForm, turma_curso: t})} cores={cores} />
|
||||
<EditInput label="Email" value={editForm.email} onChange={(t: string) => setEditForm({...editForm, email: t})} cores={cores} keyboard="email-address" />
|
||||
<EditInput label="Telemóvel" value={editForm.telefone} onChange={(t: string) => setEditForm({...editForm, telefone: t})} cores={cores} keyboard="phone-pad" />
|
||||
<EditInput label="Nascimento (AAAA-MM-DD)" value={editForm.data_nascimento} onChange={(t: string) => setEditForm({...editForm, data_nascimento: t})} cores={cores} keyboard="numeric" />
|
||||
<EditInput
|
||||
label="Nascimento (DD-MM-AAAA)"
|
||||
value={editForm.data_nascimento}
|
||||
onChange={(t: string) => setEditForm({...editForm, data_nascimento: aplicarMascaraData(t)})}
|
||||
cores={cores}
|
||||
keyboard="numeric"
|
||||
maxLength={10}
|
||||
/>
|
||||
<EditInput label="Residência" value={editForm.residencia} onChange={(t: string) => setEditForm({...editForm, residencia: t})} cores={cores} />
|
||||
|
||||
<TouchableOpacity onPress={handleUpdate} disabled={saving} style={[styles.btnSave, { backgroundColor: cores.azul }]}>
|
||||
@@ -296,20 +335,31 @@ const DetalhesAlunos = memo(() => {
|
||||
interface EditInputProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (t: string) => void;
|
||||
onChange?: (t: string) => void;
|
||||
cores: any;
|
||||
keyboard?: any;
|
||||
maxLength?: number;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
const EditInput = ({ label, value, onChange, cores, keyboard = "default" }: EditInputProps) => (
|
||||
const EditInput = ({ label, value, onChange, cores, keyboard = "default", maxLength, editable = true }: EditInputProps) => (
|
||||
<View style={{ marginBottom: 15 }}>
|
||||
<Text style={{ color: cores.secundario, fontSize: 10, fontWeight: '800', marginBottom: 5, textTransform: 'uppercase' }}>{label}</Text>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: cores.inputFundo, color: cores.texto }]}
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: cores.inputFundo,
|
||||
color: editable ? cores.texto : cores.secundario,
|
||||
opacity: editable ? 1 : 0.5 // Se não for editável, fica meio transparente
|
||||
}
|
||||
]}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
keyboardType={keyboard}
|
||||
placeholderTextColor={cores.secundario}
|
||||
maxLength={maxLength}
|
||||
editable={editable}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user