criação da empresa

This commit is contained in:
2026-04-30 10:49:08 +01:00
parent 28ce196ac4
commit aacd0ecf18
5 changed files with 537 additions and 177 deletions

View File

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

View File

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

View 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' }
});

View File

@@ -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>

View File

@@ -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>
);