atualizacoes

This commit is contained in:
2026-04-28 23:38:16 +01:00
parent 6a9a16e4ea
commit 28ce196ac4
4 changed files with 750 additions and 179 deletions

View File

@@ -6,12 +6,13 @@ import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system/legacy';
import * as Location from 'expo-location';
import { useRouter } from 'expo-router';
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
Modal,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
StatusBar,
@@ -47,17 +48,24 @@ const AlunoHome = memo(() => {
const { isDarkMode } = useTheme();
const router = useRouter();
const hojeStr = new Date().toISOString().split('T')[0];
const scrollViewRef = useRef<ScrollView>(null);
// ESTADO DOS SEPARADORES
const [activeTab, setActiveTab] = useState<'horas' | 'horario' | 'info'>('horas');
const [selectedDate, setSelectedDate] = useState(hojeStr);
const [configEstagio, setConfigEstagio] = useState({ inicio: '', fim: '' });
const [estagioDetalhes, setEstagioDetalhes] = useState<any>(null);
const [horariosEstagio, setHorariosEstagio] = useState<any[]>([]);
const [presencas, setPresencas] = 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>>({});
const [statsFaltas, setStatsFaltas] = useState({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
const [pdf, setPdf] = useState<any>(null);
const [editandoSumario, setEditandoSumario] = useState(false);
const [isLoadingDB, setIsLoadingDB] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [isLocating, setIsLocating] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [showLocationModal, setShowLocationModal] = useState(false);
@@ -74,79 +82,147 @@ const AlunoHome = memo(() => {
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
azul: azulPetroleo,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.1)',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
vermelho: '#EF4444',
verde: '#10B981',
aviso: isDarkMode ? '#2D2200' : '#FFF9E6',
avisoTexto: isDarkMode ? '#FFD700' : '#856404'
}), [isDarkMode]);
const showAlert = (msg: string, type: 'success' | 'error' | 'info' = 'info') => {
const showAlert = useCallback((msg: string, type: 'success' | 'error' | 'info' = 'info') => {
setAlertConfig({ msg, type });
Animated.sequence([
Animated.timing(alertOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
Animated.delay(3000),
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
]).start(() => setAlertConfig(null));
}, [alertOpacity]);
const fetchDadosSupabase = async (isManualRefresh = false) => {
if (!isManualRefresh) setIsLoadingDB(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: eData } = await supabase
.from('estagios')
.select('id, data_inicio, data_fim, horas_totais, horas_concluidas, horas_diarias, empresas(nome, tutor_nome, tutor_telefone)')
.eq('aluno_id', user.id)
.order('data_fim', { ascending: false })
.limit(1)
.maybeSingle();
setEstagioDetalhes(eData || null);
if (eData && eData.id) {
const { data: hData } = await supabase
.from('horarios_estagio')
.select('periodo, hora_inicio, hora_fim')
.eq('estagio_id', eData.id);
setHorariosEstagio(hData || []);
} else {
setHorariosEstagio([]);
}
if (eData && eData.data_inicio && eData.data_fim) {
const { data, error } = await supabase
.from('presencas')
.select('*')
.eq('aluno_id', user.id)
.gte('data', eData.data_inicio)
.lte('data', eData.data_fim);
if (error) throw error;
const p: any = {}, f: any = {}, s: any = {}, u: any = {};
let countJustificadas = 0;
let countInjustificadas = 0;
data?.forEach(item => {
if (item.estado === 'presente') {
p[item.data] = true;
s[item.data] = item.sumario || '';
} else {
f[item.data] = true;
u[item.data] = item.justificacao_url || '';
if (item.justificacao_url) countJustificadas++;
else countInjustificadas++;
}
});
setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u);
setStatsFaltas({ justificadas: countJustificadas, injustificadas: countInjustificadas, totalPresencas: Object.keys(p).length });
} else {
setPresencas({}); setFaltas({}); setSumarios({}); setUrlsJustificacao({});
setStatsFaltas({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
}
} catch (error) {
console.error(error);
} finally {
if (!isManualRefresh) setIsLoadingDB(false);
}
};
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, []));
const fetchDadosSupabase = async () => {
setIsLoadingDB(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: estagio } = await supabase.from('estagios').select('data_inicio, data_fim').eq('aluno_id', user.id).single();
if (estagio) {
setConfigEstagio({ inicio: estagio.data_inicio, fim: estagio.data_fim });
} else {
setConfigEstagio({ inicio: '', fim: '' });
}
useEffect(() => {
const estagiosSubscription = supabase
.channel('estagios_changes')
.on('postgres_changes', { event: '*', schema: 'public', table: 'estagios' }, () => fetchDadosSupabase())
.subscribe();
const { data, error } = await supabase.from('presencas').select('*').eq('aluno_id', user.id);
if (error) throw error;
const p: any = {}, f: any = {}, s: any = {}, u: any = {};
data?.forEach(item => {
if (item.estado === 'presente') { p[item.data] = true; s[item.data] = item.sumario || ''; }
else { f[item.data] = true; u[item.data] = item.justificacao_url || ''; }
});
setPresencas(p); setFaltas(f); setSumarios(s); setUrlsJustificacao(u);
} catch (error) {
console.error(error);
} finally {
setIsLoadingDB(false);
}
};
const presencasSubscription = supabase
.channel('presencas_changes')
.on('postgres_changes', { event: '*', schema: 'public', table: 'presencas' }, () => fetchDadosSupabase())
.subscribe();
return () => {
supabase.removeChannel(estagiosSubscription);
supabase.removeChannel(presencasSubscription);
};
}, []);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchDadosSupabase(true);
setRefreshing(false);
}, []);
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
// LOGICA CORRIGIDA: Se as datas forem '', o estágio não existe e bloqueia tudo
const statusEstagio = useMemo(() => {
if (!estagioDetalhes || !estagioDetalhes.data_inicio || !estagioDetalhes.data_fim) return null;
if (hojeStr > estagioDetalhes.data_fim) return 'concluido';
if (hojeStr < estagioDetalhes.data_inicio) return 'agendado';
return 'ativo';
}, [estagioDetalhes, hojeStr]);
const infoData = useMemo(() => {
const data = new Date(selectedDate);
const diaSemana = data.getDay();
const nomeFeriado = feriadosMap[selectedDate];
const temEstagio = configEstagio.inicio !== '' && configEstagio.fim !== '';
const antesDoInicio = temEstagio && selectedDate < configEstagio.inicio;
const depoisDoFim = temEstagio && selectedDate > configEstagio.fim;
const temEstagio = !!estagioDetalhes && estagioDetalhes.data_inicio && estagioDetalhes.data_fim;
const antesDoInicio = temEstagio && selectedDate < estagioDetalhes.data_inicio;
const depoisDoFim = temEstagio && selectedDate > estagioDetalhes.data_fim;
const estagioAtivo = statusEstagio === 'ativo';
return {
valida: temEstagio && diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado,
podeMarcar: temEstagio && selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado,
nomeFeriado,
antesDoInicio,
depoisDoFim,
foraDeRange: !temEstagio || antesDoInicio || depoisDoFim,
temEstagio
valida: estagioAtivo && diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado,
podeMarcar: estagioAtivo && selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado,
nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: !temEstagio || antesDoInicio || depoisDoFim,
temEstagio, estagioAtivo
};
}, [selectedDate, configEstagio, hojeStr, feriadosMap]);
}, [selectedDate, estagioDetalhes, hojeStr, feriadosMap, statusEstagio]);
const handlePresencaClick = async () => {
if (!infoData.temEstagio) {
showAlert("Aguarde pela configuração do estágio.", "error");
return;
}
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");
const { status } = await Location.getForegroundPermissionsAsync();
if (status === 'granted') {
executarMarcacao();
@@ -167,7 +243,6 @@ const AlunoHome = memo(() => {
aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude
});
showAlert("Presença marcada!", "success");
fetchDadosSupabase();
} catch (e: any) { showAlert(e.message, "error"); }
finally { setIsLocating(false); }
};
@@ -179,7 +254,6 @@ const AlunoHome = memo(() => {
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' });
showAlert("Falta registada.", "info");
fetchDadosSupabase();
} catch (e) { showAlert("Erro ao registar falta.", "error"); }
};
@@ -201,7 +275,6 @@ const AlunoHome = memo(() => {
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
setPdf(null);
showAlert("Enviado com sucesso!", "success");
fetchDadosSupabase();
} catch (e) { showAlert("Erro no upload.", "error"); }
finally { setIsUploading(false); }
};
@@ -212,10 +285,20 @@ const AlunoHome = memo(() => {
await supabase.from('presencas').update({ sumario: sumarios[selectedDate] }).match({ aluno_id: user?.id, data: selectedDate });
setEditandoSumario(false);
showAlert("Sumário guardado!", "success");
fetchDadosSupabase();
} catch (e) { showAlert("Erro ao guardar.", "error"); }
};
const getBadgeStyle = () => {
if (statusEstagio === 'concluido') return { bg: '#E2E8F0', text: '#475569', label: 'CONCLUÍDO' };
if (statusEstagio === 'agendado') return { bg: '#FEF3C7', text: '#D97706', label: 'AGENDADO' };
return { bg: themeStyles.verde + '20', text: themeStyles.verde, label: 'A DECORRER' };
};
const badgeObj = getBadgeStyle();
const horasTotais = estagioDetalhes?.horas_totais || 0;
const horasConcluidas = estagioDetalhes?.horas_concluidas || 0;
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
@@ -247,7 +330,14 @@ const AlunoHome = memo(() => {
</Animated.View>
)}
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<ScrollView
ref={scrollViewRef}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />
}
>
<View style={styles.topBar}>
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
<View style={styles.topIcons}>
@@ -260,34 +350,216 @@ const AlunoHome = memo(() => {
</View>
</View>
{/* AVISO DE FALTA DE ESTÁGIO - Vai dar merda se o aluno não souber por que está bloqueado */}
{!infoData.temEstagio && !isLoadingDB && (
{/* 🚀 BOTÕES DE SEPARADOR (TABS) MODERNOS 🚀 */}
<View style={[styles.quickActionsContainer, { backgroundColor: isDarkMode ? '#1E1E1E' : '#F1F5F9' }]}>
<TouchableOpacity
style={[
styles.quickActionBtn,
activeTab === 'horas' && styles.quickActionBtnActive,
activeTab === 'horas' && { backgroundColor: themeStyles.card }
]}
onPress={() => setActiveTab('horas')}
activeOpacity={0.7}
>
<Ionicons
name={activeTab === 'horas' ? "pie-chart" : "pie-chart-outline"}
size={18}
color={activeTab === 'horas' ? themeStyles.azul : themeStyles.textoSecundario}
/>
<Text style={[styles.quickActionText, { color: activeTab === 'horas' ? themeStyles.azul : themeStyles.textoSecundario }]}>
Horas
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.quickActionBtn,
activeTab === 'horario' && styles.quickActionBtnActive,
activeTab === 'horario' && { backgroundColor: themeStyles.card }
]}
onPress={() => setActiveTab('horario')}
activeOpacity={0.7}
>
<Ionicons
name={activeTab === 'horario' ? "time" : "time-outline"}
size={18}
color={activeTab === 'horario' ? themeStyles.laranja : themeStyles.textoSecundario}
/>
<Text style={[styles.quickActionText, { color: activeTab === 'horario' ? themeStyles.laranja : themeStyles.textoSecundario }]}>
Horário
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.quickActionBtn,
activeTab === 'info' && styles.quickActionBtnActive,
activeTab === 'info' && { backgroundColor: themeStyles.card }
]}
onPress={() => setActiveTab('info')}
activeOpacity={0.7}
>
<Ionicons
name={activeTab === 'info' ? "information-circle" : "information-circle-outline"}
size={18}
color={activeTab === 'info' ? themeStyles.verde : themeStyles.textoSecundario}
/>
<Text style={[styles.quickActionText, { color: activeTab === 'info' ? themeStyles.verde : themeStyles.textoSecundario }]}>
Info
</Text>
</TouchableOpacity>
</View>
{/* CARTÃO PRINCIPAL QUE MUDA CONFORME O SEPARADOR ATIVO */}
{!isLoadingDB && (
estagioDetalhes ? (
<View style={[styles.dashboardCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul }]}>
{/* O Cabeçalho (Empresa e Status) fica sempre visível */}
<View style={styles.dashHeader}>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: 8 }}>
<Ionicons name="business" size={20} color={statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul} />
<Text style={[styles.dashEmpresa, { color: themeStyles.texto }]} numberOfLines={1}>
{estagioDetalhes.empresas?.nome || "Empresa não definida"}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: badgeObj.bg }]}>
<Text style={[styles.statusBadgeText, { color: badgeObj.text }]}>{badgeObj.label}</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginTop: 4 }]} />
{/* CONTEÚDO 1: HORAS */}
{activeTab === 'horas' && (
<View>
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>REALIZADAS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{horasConcluidas}h</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>EM FALTA</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.laranja }]}>{horasEmFalta}h</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>TOTAIS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.texto }]}>{horasTotais}h</Text>
</View>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<View style={styles.dashGrid}>
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>PRESENÇAS</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.azul }]}>{statsFaltas.totalPresencas}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>FALTAS JUST.</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.verde }]}>{statsFaltas.justificadas}</Text>
</View>
<View style={styles.dashDividerVertical} />
<View style={styles.dashGridItem}>
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>FALTAS INJ.</Text>
<Text style={[styles.dashStatValue, { color: themeStyles.vermelho }]}>{statsFaltas.injustificadas}</Text>
</View>
</View>
</View>
)}
{/* CONTEÚDO 2: HORÁRIO DIÁRIO */}
{activeTab === 'horario' && (
<View style={{ alignItems: 'center', paddingVertical: 10 }}>
<Ionicons name="time" size={40} color={themeStyles.laranja} style={{ marginBottom: 10 }} />
<Text style={{ fontSize: 13, color: themeStyles.textoSecundario, fontWeight: '700', textTransform: 'uppercase' }}>Carga Horária</Text>
<Text style={{ fontSize: 24, fontWeight: '900', color: themeStyles.texto, marginTop: 2 }}>
{estagioDetalhes.horas_diarias ? estagioDetalhes.horas_diarias + '/dia' : 'Não definido'}
</Text>
{/* Mostra a Manhã e a Tarde se existirem na base de dados */}
{horariosEstagio.length > 0 && (
<View style={{ width: '100%', marginTop: 20 }}>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginVertical: 8 }]} />
{horariosEstagio.map((h, index) => (
<View key={index} style={{ flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 8, paddingHorizontal: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<Ionicons name={h.periodo === 'Manhã' ? "partly-sunny-outline" : "sunny-outline"} size={16} color={themeStyles.textoSecundario} />
<Text style={{ fontSize: 15, fontWeight: '800', color: themeStyles.textoSecundario }}>{h.periodo}</Text>
</View>
<Text style={{ fontSize: 15, fontWeight: '900', color: themeStyles.texto }}>
{h.hora_inicio?.slice(0, 5)} - {h.hora_fim?.slice(0, 5)}
</Text>
</View>
))}
</View>
)}
</View>
)}
{/* CONTEÚDO 3: INFO GERAL */}
{activeTab === 'info' && (
<View>
{/* Tutor e Contacto */}
<View style={styles.infoRow}>
<Ionicons name="person" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Tutor da Empresa</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_nome || "N/A"}</Text>
</View>
</View>
<View style={styles.infoRow}>
<Ionicons name="call" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Contacto</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_telefone || "N/A"}</Text>
</View>
</View>
{/* Datas */}
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Data de Início</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_inicio}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Data de Fim (Prevista)</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.data_fim}</Text>
</View>
</View>
</View>
)}
</View>
) : (
<View style={[styles.avisoBox, { backgroundColor: themeStyles.aviso }]}>
<Ionicons name="alert-circle" size={20} color={themeStyles.avisoTexto} />
<Ionicons name="information-circle" size={24} color={themeStyles.avisoTexto} />
<Text style={[styles.avisoTexto, { color: themeStyles.avisoTexto }]}>
O teu período de estágio ainda não foi configurado pelo professor.
Sem estágio atribuído no sistema. Aguarda indicação do teu professor.
</Text>
</View>
)
)}
<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 || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
onPress={handlePresencaClick}
disabled={!infoData.podeMarcar || !!presencas[selectedDate] || !!faltas[selectedDate] || 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 || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
onPress={handleFalta}
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
>
@@ -298,26 +570,24 @@ const AlunoHome = memo(() => {
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<Calendar
key={isDarkMode ? 'dark' : 'light'}
minDate={configEstagio.inicio || undefined}
maxDate={configEstagio.fim || undefined}
theme={{
calendarBackground: themeStyles.card,
dayTextColor: themeStyles.texto,
monthTextColor: themeStyles.texto,
todayTextColor: azulPetroleo,
selectedDayBackgroundColor: azulPetroleo,
textDisabledColor: isDarkMode ? '#333' : '#DDD'
calendarBackground: themeStyles.card, dayTextColor: themeStyles.texto, monthTextColor: themeStyles.texto,
todayTextColor: azulPetroleo, selectedDayBackgroundColor: azulPetroleo, textDisabledColor: isDarkMode ? '#333' : '#DDD'
}}
markedDates={{
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
...Object.keys(presencas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#10B981' } }), {}),
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#EF4444' } }), {}),
...Object.keys(presencas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.verde } }), {}),
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
}}
onDayPress={(day) => setSelectedDate(day.dateString)}
/>
</View>
{infoData.nomeFeriado && (
<Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.textoSecundario }}>🎉 {infoData.nomeFeriado}</Text>
)}
{presencas[selectedDate] && (
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
<View style={styles.rowTitle}>
@@ -326,11 +596,9 @@ const AlunoHome = memo(() => {
</View>
<TextInput
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto }]}
multiline editable={editandoSumario}
value={sumarios[selectedDate]}
multiline editable={editandoSumario} value={sumarios[selectedDate]}
onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})}
placeholder="O que fizeste hoje?"
placeholderTextColor="#94A3B8"
placeholder="O que fizeste hoje?" placeholderTextColor="#94A3B8"
/>
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar Sumário</Text></TouchableOpacity>}
</View>
@@ -366,14 +634,66 @@ const AlunoHome = memo(() => {
const styles = StyleSheet.create({
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
container: { padding: 20 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 25 },
container: { padding: 20, paddingBottom: 40 },
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
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'
},
// 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' },
statusBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
statusBadgeText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
dashGrid: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
dashGridItem: { flex: 1, alignItems: 'center' },
dashStatLabel: { fontSize: 10, textTransform: 'uppercase', fontWeight: '800', marginBottom: 4, textAlign: 'center' },
dashStatValue: { fontSize: 17, fontWeight: '900', textAlign: 'center' },
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' },
alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 15, zIndex: 1000 },
alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' },
avisoBox: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 15, borderRadius: 15, marginBottom: 20 },
avisoTexto: { fontSize: 13, fontWeight: '700', flex: 1 },
avisoBox: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 18, borderRadius: 18, marginBottom: 20 },
avisoTexto: { fontSize: 14, fontWeight: '700', flex: 1, lineHeight: 20 },
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
btn: { padding: 18, borderRadius: 22, width: '48%', alignItems: 'center', elevation: 3 },
txtBtn: { color: '#fff', fontWeight: '800', fontSize: 14 },

View File

@@ -1,9 +1,8 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Animated,
KeyboardAvoidingView,
Platform,
@@ -27,11 +26,11 @@ export default function PerfilAluno() {
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [perfil, setPerfil] = useState<any>(null);
const [estagio, setEstagio] = useState<any>(null);
const [saving, setSaving] = useState(false);
const [faltasJustificadas, setFaltasJustificadas] = useState(0);
const [faltasInjustificadas, setFaltasInjustificadas] = useState(0);
// --- 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), []);
@@ -48,19 +47,15 @@ export default function PerfilAluno() {
verde: '#10B981',
}), [isDarkMode]);
// --- LÓGICA DE VERIFICAÇÃO DO ESTÁGIO ATIVO ---
const isEstagioAtivo = useMemo(() => {
if (!estagio) return false; // Se não tem estágio na DB, não está ativo
if (!estagio.data_fim) return true; // Se tem estágio mas sem data de fim, assumimos ativo
const hoje = new Date();
hoje.setHours(0, 0, 0, 0); // Reset às horas para comparar só o dia
const dataFim = new Date(estagio.data_fim);
dataFim.setHours(0, 0, 0, 0);
return dataFim >= hoje; // Se a data de fim for hoje ou no futuro, está ativo
}, [estagio]);
// --- FUNÇÃO DE MOSTRAR O ALERTA ---
const showAlert = useCallback((msg: string, type: 'success' | 'error') => {
setAlertConfig({ msg, type });
Animated.sequence([
Animated.timing(alertOpacity, { toValue: 1, duration: 400, useNativeDriver: true }),
Animated.delay(2500),
Animated.timing(alertOpacity, { toValue: 0, duration: 400, useNativeDriver: true })
]).start(() => setAlertConfig(null));
}, [alertOpacity]);
// --- FUNÇÕES DE DATA ---
const formatarDataParaUI = (dataDB: string) => {
@@ -110,8 +105,6 @@ export default function PerfilAluno() {
return formatted;
};
// --- FIM FUNÇÕES DATA ---
const buscarDados = async () => {
try {
setLoading(true);
@@ -126,19 +119,6 @@ export default function PerfilAluno() {
if (alunoRes) aData = alunoRes;
}
const { data: eData } = await supabase
.from('estagios')
.select(`*, empresas(*), horarios_estagio(*)`)
.eq('aluno_id', user.id)
.maybeSingle();
const { data: presencas } = await supabase.from('presencas').select('estado, justificacao_url').eq('aluno_id', user.id);
if (presencas) {
setFaltasJustificadas(presencas.filter(p => p.estado === 'Falta' && p.justificacao_url).length);
setFaltasInjustificadas(presencas.filter(p => p.estado === 'Falta' && !p.justificacao_url).length);
}
const dataFormatadaUI = formatarDataParaUI(pData?.data_nascimento);
const idadeCalculada = dataFormatadaUI ? calcularIdade(dataFormatadaUI) : pData?.idade;
@@ -149,8 +129,6 @@ export default function PerfilAluno() {
data_nascimento: dataFormatadaUI,
idade: idadeCalculada ?? 'N/A'
});
setEstagio(eData);
Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start();
} catch (err) {
@@ -181,14 +159,15 @@ export default function PerfilAluno() {
if (error) throw error;
if (!data || data.length === 0) {
throw new Error("Erro de RLS. Confirma as tuas políticas no Supabase.");
throw new Error("Erro nas permissões. Confirma as tuas políticas no Supabase.");
}
setIsEditing(false);
Alert.alert("Sucesso", "Perfil atualizado!");
// 🔥 O NOSSO NOVO ALERTA MODERNO EM ACÇÃO 🔥
showAlert("Perfil guardado com sucesso!", "success");
await buscarDados();
} catch (e: any) {
Alert.alert("Erro ao gravar", e.message);
showAlert(e.message || "Erro ao guardar alterações.", "error");
} finally {
setSaving(false);
}
@@ -199,6 +178,22 @@ export default function PerfilAluno() {
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,
{
opacity: alertOpacity,
backgroundColor: alertConfig.type === 'success' ? cores.verde : cores.vermelho,
top: insets.top > 0 ? insets.top + 10 : 40
}
]}>
<Ionicons name={alertConfig.type === 'success' ? "checkmark-circle" : "alert-circle"} size={22} color="#fff" />
<Text style={styles.modernAlertText}>{alertConfig.msg}</Text>
</Animated.View>
)}
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<View style={styles.headerContainer}>
@@ -274,39 +269,6 @@ export default function PerfilAluno() {
<PerfilInput label="Residência" icon="location-outline" value={perfil?.residencia} editable={isEditing} onChangeText={(v:string) => setPerfil({...perfil, residencia: v})} cores={cores} />
</View>
<Text style={[styles.sectionHeader, { color: cores.secundario }]}>Informação de Estágio</Text>
{/* Aqui entra a condição: Só mostra a info se isEstagioAtivo for verdadeiro */}
{isEstagioAtivo ? (
<View style={[styles.infoCard, { backgroundColor: cores.card, borderLeftWidth: 4, borderLeftColor: cores.azul }]}>
<PerfilInput label="Entidade de Acolhimento" icon="business-outline" value={estagio.empresas?.nome} editable={false} cores={cores} />
<View style={styles.inputRow}>
<View style={{ flex: 1, marginRight: 10 }}>
<PerfilInput label="Horas Totais" icon="timer-outline" value={`${estagio.horas_totais}h`} editable={false} cores={cores} />
</View>
<View style={{ flex: 1 }}>
<PerfilInput label="Realizadas" icon="checkmark-done-outline" value={`${estagio.horas_concluidas || 0}h`} editable={false} cores={cores} />
</View>
</View>
<View style={[styles.inputRow, { marginTop: 10 }]}>
<View style={[styles.miniStatus, { backgroundColor: cores.azulSuave, flex: 1, marginRight: 10 }]}>
<Text style={[styles.miniStatusText, { color: cores.azul }]}>JUSTIFICADAS: {faltasJustificadas}</Text>
</View>
<View style={[styles.miniStatus, { backgroundColor: cores.vermelhoSuave, flex: 1 }]}>
<Text style={[styles.miniStatusText, { color: cores.vermelho }]}>INJUSTIFICADAS: {faltasInjustificadas}</Text>
</View>
</View>
</View>
) : (
<View style={[styles.infoCard, { backgroundColor: cores.card, alignItems: 'center', padding: 30, borderStyle: 'dashed', borderWidth: 1, borderColor: cores.borda }]}>
<Ionicons name="warning-outline" size={30} color={cores.secundario} />
{/* Mensagem dinâmica se já teve ou se nunca teve estágio */}
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700', textAlign: 'center' }}>
{estagio ? "Sem estágio ativo no momento." : "Sem estágio atribuído no sistema"}
</Text>
</View>
)}
<View style={styles.footer}>
<TouchableOpacity style={[styles.actionMenuItem, { backgroundColor: cores.card }]} onPress={() => router.push('/redefenirsenha')}>
<View style={[styles.actionIcon, { backgroundColor: cores.azulSuave }]}>
@@ -351,6 +313,31 @@ 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
},
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 },
@@ -368,8 +355,6 @@ const styles = StyleSheet.create({
inputContainer: { flexDirection: 'row', alignItems: 'center', borderRadius: 14, borderWidth: 1.5, height: 52 },
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
inputRow: { flexDirection: 'row', justifyContent: 'space-between' },
miniStatus: { padding: 12, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
miniStatusText: { fontSize: 10, fontWeight: '900' },
footer: { marginTop: 25 },
actionMenuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 3 },
actionIcon: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },

View File

@@ -4,7 +4,6 @@ import { useRouter } from 'expo-router';
import { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Modal,
ScrollView,
StatusBar,
@@ -28,10 +27,21 @@ interface Estagio {
data_inicio: string;
data_fim: string;
horas_diarias?: string;
horas_totais?: number;
alunos: { nome: string; turma_curso: string; ano: number };
empresas: { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string };
}
// Configuração PT-PT para Feriados e afins
const getFeriadosMap = (ano: number) => ({
[`${ano}-01-01`]: "Ano Novo", [`${ano}-04-25`]: "Dia da Liberdade",
[`${ano}-05-01`]: "Dia do Trabalhador", [`${ano}-06-10`]: "Dia de Portugal",
[`${ano}-06-24`]: "São João (Vila do Conde)", [`${ano}-08-15`]: "Assunção de Nª Senhora",
[`${ano}-10-05`]: "Implantação da República", [`${ano}-11-01`]: "Todos os Santos",
[`${ano}-12-01`]: "Restauração da Independência", [`${ano}-12-08`]: "Imaculada Conceição",
[`${ano}-12-25`]: "Natal"
});
export default function Estagios() {
const router = useRouter();
const { isDarkMode } = useTheme();
@@ -48,6 +58,8 @@ export default function Estagios() {
azul: azulEPVC,
laranja: laranjaEPVC,
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.12)' : '#FFF5F5',
laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.12)' : '#FFF9F0',
vermelho: '#EF4444',
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
overlay: 'rgba(26, 54, 93, 0.8)',
@@ -60,20 +72,32 @@ export default function Estagios() {
const [loading, setLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
// ESTADO DO ALERTA CUSTOMIZADO (AGORA CENTRADO E BONITO)
const [customAlert, setCustomAlert] = useState({ visible: false, title: '', msg: '', tipo: 'warning' as 'warning' | 'error' });
const [estagioParaApagar, setEstagioParaApagar] = useState<{id: string, nome: string} | null>(null);
const [passo, setPasso] = useState(1);
const [alunoSelecionado, setAlunoSelecionado] = useState<Aluno | null>(null);
const [empresaSelecionada, setEmpresaSelecionada] = useState<Empresa | null>(null);
const [editandoEstagio, setEditandoEstagio] = useState<Estagio | null>(null);
// Datas e Horas Totais
const [dataInicio, setDataInicio] = useState('');
const [dataFim, setDataFim] = useState('');
const [horasTotaisEstagio, setHorasTotaisEstagio] = useState('');
const [searchMain, setSearchMain] = useState('');
// Horários Diários
const [hManhaIni, setHManhaIni] = useState('');
const [hManhaFim, setHManhaFim] = useState('');
const [hTardeIni, setHTardeIni] = useState('');
const [hTardeFim, setHTardeFim] = useState('');
const showCustomAlert = (title: string, msg: string, tipo: 'warning' | 'error' = 'warning') => {
setCustomAlert({ visible: true, title, msg, tipo });
};
useEffect(() => { fetchDados(); }, []);
const fetchDados = async () => {
@@ -106,6 +130,14 @@ export default function Estagios() {
}
};
const aplicarMascaraHora = (value: string) => {
const cleaned = value.replace(/\D/g, '');
if (cleaned.length >= 3) {
return `${cleaned.slice(0, 2)}:${cleaned.slice(2, 4)}`;
}
return cleaned;
};
const totalHorasDiarias = useMemo(() => {
const calcularMinutos = (ini: string, fim: string) => {
if (!ini || !fim || !ini.includes(':') || !fim.includes(':')) return 0;
@@ -127,10 +159,93 @@ export default function Estagios() {
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`;
};
const calcularMagia = (tipo: 'dataFim' | 'horasTotais') => {
if (!dataInicio) return showCustomAlert("Atenção", "Preenche a Data de Início primeiro!");
if (!hManhaIni && !hTardeIni) return showCustomAlert("Atenção", "Preenche primeiro o horário diário (Manhã e/ou Tarde)!");
const matchH = totalHorasDiarias.match(/(\d+)h/);
const matchM = totalHorasDiarias.match(/(\d+)m/);
const horasD = (matchH ? parseInt(matchH[1]) : 0) + (matchM ? parseInt(matchM[1]) / 60 : 0);
if (horasD <= 0) return showCustomAlert("Atenção", "O horário diário tem de ser superior a zero.", 'error');
if (tipo === 'dataFim') {
const totais = parseInt(horasTotaisEstagio);
if (!totais || totais <= 0) return showCustomAlert("Atenção", "Preenche as Horas Totais primeiro!");
let diasNecessarios = Math.ceil(totais / horasD);
let currentDate = new Date(dataInicio);
let diasContados = 0;
while (diasContados < diasNecessarios) {
const day = currentDate.getDay();
const dateStr = currentDate.toISOString().split('T')[0];
const feriadosMapAno = getFeriadosMap(currentDate.getFullYear());
if (day !== 0 && day !== 6 && !feriadosMapAno[dateStr]) {
diasContados++;
}
if (diasContados < diasNecessarios) {
currentDate.setDate(currentDate.getDate() + 1);
}
}
setDataFim(currentDate.toISOString().split('T')[0]);
} else if (tipo === 'horasTotais') {
if (!dataFim) return showCustomAlert("Atenção", "Preenche a Data Fim (Prev.) primeiro!");
let currentDate = new Date(dataInicio);
const endDate = new Date(dataFim);
let diasUteis = 0;
if (currentDate > endDate) return showCustomAlert("Atenção", "A Data de Início não pode ser depois da Data Fim!", 'error');
while (currentDate <= endDate) {
const day = currentDate.getDay();
const dateStr = currentDate.toISOString().split('T')[0];
const feriadosMapAno = getFeriadosMap(currentDate.getFullYear());
if (day !== 0 && day !== 6 && !feriadosMapAno[dateStr]) {
diasUteis++;
}
currentDate.setDate(currentDate.getDate() + 1);
}
const calculoFinal = Math.round(diasUteis * horasD);
setHorasTotaisEstagio(calculoFinal.toString());
}
};
const salvarEstagio = async () => {
const matchH = totalHorasDiarias.match(/(\d+)h/);
const matchM = totalHorasDiarias.match(/(\d+)m/);
const horasD = (matchH ? parseInt(matchH[1]) : 0) + (matchM ? parseInt(matchM[1]) / 60 : 0);
if (horasD <= 0) {
return showCustomAlert("Erro de Horário", "O aluno tem de ter um horário válido (maior que 0 horas).", 'error');
}
if (horasD > 8) {
return showCustomAlert(
"Limite Legal Excedido",
"Por lei, um aluno em Formação em Contexto de Trabalho (FCT) não pode exceder as 8 horas diárias.",
'error'
);
}
if (dataFim && dataInicio > dataFim) {
return showCustomAlert("Datas Inválidas", "A data de fim do estágio não pode ser anterior à data de início.", 'error');
}
const totais = parseInt(horasTotaisEstagio);
if (!totais || totais <= 0) {
return showCustomAlert("Horas Totais", "Indica o número total de horas que o aluno tem de cumprir (Ex: 400).", 'warning');
}
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (empresaSelecionada) {
await supabase.from('empresas').update({
tutor_nome: empresaSelecionada.tutor_nome,
@@ -145,6 +260,7 @@ export default function Estagios() {
data_inicio: dataInicio || new Date().toISOString().split('T')[0],
data_fim: dataFim || null,
horas_diarias: totalHorasDiarias,
horas_totais: totais,
estado: 'Ativo',
};
@@ -167,7 +283,11 @@ export default function Estagios() {
handleFecharModal();
fetchDados();
} catch (error: any) { Alert.alert("Erro", error.message); } finally { setLoading(false); }
} catch (error: any) {
showCustomAlert("Erro ao Guardar", error.message, 'error');
} finally {
setLoading(false);
}
};
const confirmarEliminacao = async () => {
@@ -180,7 +300,7 @@ export default function Estagios() {
const handleFecharModal = () => {
setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
setDataInicio(''); setDataFim(''); setHorasTotaisEstagio(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
setPasso(1);
};
@@ -199,7 +319,6 @@ export default function Estagios() {
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
{/* HEADER EPVC */}
<View style={styles.header}>
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={cores.azul}/>
@@ -214,7 +333,6 @@ export default function Estagios() {
</View>
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]} showsVerticalScrollIndicator={false}>
{/* SEARCH MODERNO */}
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<Ionicons name="search-outline" size={20} color={cores.azul} />
<TextInput
@@ -244,7 +362,9 @@ export default function Estagios() {
setEditandoEstagio(e);
setAlunoSelecionado(alunos.find(a => a.id === e.aluno_id) || null);
setEmpresaSelecionada(empresas.find(emp => emp.id === e.empresa_id) || null);
setDataInicio(e.data_inicio || ''); setDataFim(e.data_fim || '');
setDataInicio(e.data_inicio || '');
setDataFim(e.data_fim || '');
setHorasTotaisEstagio(e.horas_totais?.toString() || '');
carregarHorariosEdicao(e.id);
setPasso(2); setModalVisible(true);
}}
@@ -264,9 +384,12 @@ export default function Estagios() {
<Text style={[styles.empresaText, { color: cores.secundario }]} numberOfLines={1}>{e.empresas?.nome}</Text>
</View>
</View>
<View style={[styles.timeBadge, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Ionicons name="time-outline" size={12} color={cores.azul} />
<Text style={[styles.timeText, { color: cores.texto }]}>{e.horas_diarias}</Text>
<View style={{ alignItems: 'flex-end' }}>
<View style={[styles.timeBadge, { backgroundColor: cores.fundo, borderColor: cores.borda, marginBottom: 4 }]}>
<Ionicons name="time-outline" size={12} color={cores.azul} />
<Text style={[styles.timeText, { color: cores.texto }]}>{e.horas_diarias}</Text>
</View>
<Text style={{ fontSize: 10, color: cores.secundario, fontWeight: '700' }}>{e.horas_totais || 0}h Totais</Text>
</View>
</View>
</TouchableOpacity>
@@ -275,7 +398,6 @@ export default function Estagios() {
))}
</ScrollView>
{/* FAB EPVC */}
<TouchableOpacity
style={[styles.fab, { backgroundColor: cores.laranja, bottom: insets.bottom + 20 }]}
onPress={() => { setPasso(1); setModalVisible(true); }}
@@ -358,10 +480,39 @@ export default function Estagios() {
) : (
<View style={{ gap: 18 }}>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.groupTitle, { color: cores.azul }]}>Cronograma</Text>
<Text style={[styles.groupTitle, { color: cores.azul }]}>Cronograma e Horas</Text>
<View style={styles.rowInputs}>
<View style={{flex:1}}><Text style={styles.miniLabel}>DATA INÍCIO</Text><TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataInicio} onChangeText={setDataInicio} placeholder="AAAA-MM-DD"/></View>
<View style={{flex:1}}><Text style={styles.miniLabel}>DATA FIM (PREV.)</Text><TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataFim} onChangeText={setDataFim} placeholder="AAAA-MM-DD"/></View>
<View style={{flex:1}}>
<Text style={styles.miniLabel}>DATA INÍCIO</Text>
<TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataInicio} onChangeText={setDataInicio} placeholder="AAAA-MM-DD"/>
</View>
<View style={{flex:1}}>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6}}>
<Text style={[styles.miniLabel, {marginBottom: 0}]}>DATA FIM (PREV.)</Text>
<TouchableOpacity onPress={() => calcularMagia('dataFim')} hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}>
<Ionicons name="color-wand" size={16} color={cores.laranja} />
</TouchableOpacity>
</View>
<TextInput style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]} value={dataFim} onChangeText={setDataFim} placeholder="AAAA-MM-DD"/>
</View>
</View>
<View style={{marginTop: 15}}>
<View style={{flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6}}>
<Text style={[styles.miniLabel, {marginBottom: 0}]}>HORAS TOTAIS DO ESTÁGIO</Text>
<TouchableOpacity onPress={() => calcularMagia('horasTotais')} style={{flexDirection: 'row', alignItems: 'center', gap: 4}} hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}>
<Ionicons name="color-wand" size={16} color={cores.laranja} />
<Text style={{fontSize: 10, fontWeight: '800', color: cores.laranja}}>AUTO-CALCULAR</Text>
</TouchableOpacity>
</View>
<TextInput
style={[styles.modernInput, { color: cores.texto, backgroundColor: cores.card }]}
value={horasTotaisEstagio}
onChangeText={setHorasTotaisEstagio}
keyboardType="numeric"
placeholder="Ex: 400"
/>
</View>
</View>
@@ -372,13 +523,41 @@ export default function Estagios() {
</View>
<View style={styles.tabelaRow}>
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Manhã</Text>
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hManhaIni} onChangeText={setHManhaIni} placeholder="09:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hManhaFim} onChangeText={setHManhaFim} placeholder="13:00" />
<TextInput
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
value={hManhaIni}
onChangeText={(txt) => setHManhaIni(aplicarMascaraHora(txt))}
keyboardType="numeric"
maxLength={5}
placeholder="09:00"
/>
<TextInput
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
value={hManhaFim}
onChangeText={(txt) => setHManhaFim(aplicarMascaraHora(txt))}
keyboardType="numeric"
maxLength={5}
placeholder="13:00"
/>
</View>
<View style={styles.tabelaRow}>
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Tarde</Text>
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hTardeIni} onChangeText={setHTardeIni} placeholder="14:00" />
<TextInput style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]} value={hTardeFim} onChangeText={setHTardeFim} placeholder="18:00" />
<TextInput
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
value={hTardeIni}
onChangeText={(txt) => setHTardeIni(aplicarMascaraHora(txt))}
keyboardType="numeric"
maxLength={5}
placeholder="14:00"
/>
<TextInput
style={[styles.tabelaInput, {color: cores.texto, backgroundColor: cores.card}]}
value={hTardeFim}
onChangeText={(txt) => setHTardeFim(aplicarMascaraHora(txt))}
keyboardType="numeric"
maxLength={5}
placeholder="18:00"
/>
</View>
</View>
@@ -401,7 +580,7 @@ export default function Estagios() {
onPress={() => {
if(passo === 1) {
if(alunoSelecionado && empresaSelecionada) setPasso(2);
else Alert.alert("Atenção", "Selecione o aluno e a empresa.");
else showCustomAlert("Atenção", "Selecione o aluno e a empresa antes de avançar.");
} else salvarEstagio();
}}
style={[styles.btnModalPri, { backgroundColor: cores.azul }]}
@@ -413,12 +592,52 @@ export default function Estagios() {
</View>
</Modal>
{/* DELETE MODAL */}
{/* CUSTOM ALERT MODAL (CENTRADINHO E IGUAL À FOTO) */}
<Modal
visible={customAlert.visible}
transparent
animationType="fade"
onRequestClose={() => setCustomAlert({ ...customAlert, visible: false })}
>
<View style={styles.alertOverlay}>
<View style={[styles.alertCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
{/* Botão de fechar (X vermelho pequeno) centrado no topo */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() => setCustomAlert({ ...customAlert, visible: false })}
style={[styles.alertIconBg, { backgroundColor: customAlert.tipo === 'error' ? cores.vermelho : cores.laranja }]}
>
<Ionicons
name={customAlert.tipo === 'error' ? "close" : "warning"}
size={20}
color="#fff"
/>
</TouchableOpacity>
<Text style={[styles.alertTitle, { color: cores.texto }]}>
{customAlert.title.replace(' ⚖️', '')}
</Text>
{/* Mostra a balança apenas se for o erro do Limite Legal */}
{customAlert.title.includes('Excedido') && (
<Text style={styles.alertEmoji}></Text>
)}
<Text style={[styles.alertSubtitle, { color: cores.secundario }]}>
{customAlert.msg}
</Text>
</View>
</View>
</Modal>
{/* DELETE MODAL ORIGINAL */}
<Modal visible={deleteModalVisible} transparent animationType="fade">
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
<View style={[styles.deleteCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
<View style={[styles.deleteIconBg, { backgroundColor: isDarkMode ? '#2D1616' : '#FFF5F5' }]}>
<Ionicons name="warning" size={35} color={cores.vermelho} />
<View style={[styles.deleteIconBg, { backgroundColor: cores.vermelhoSuave }]}>
<Ionicons name="trash" size={35} color={cores.vermelho} />
</View>
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Cancelar Estágio?</Text>
<Text style={[styles.deleteSubtitle, { color: cores.secundario }]}>
@@ -435,6 +654,7 @@ export default function Estagios() {
</View>
</View>
</Modal>
</View>
);
}
@@ -462,7 +682,7 @@ const styles = StyleSheet.create({
timeText: { fontSize: 11, fontWeight: '900' },
fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 10 },
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase', letterSpacing: 0.5 },
modalOverlay: { flex: 1, justifyContent: 'flex-end' },
modalOverlay: { flex: 1, justifyContent: 'flex-end', alignItems: 'center' },
modalContent: { borderTopLeftRadius: 45, borderTopRightRadius: 45, padding: 28, height: '92%', width: '100%' },
modalIndicator: { width: 45, height: 5, backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 10, alignSelf: 'center', marginBottom: 20 },
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 },
@@ -470,7 +690,7 @@ const styles = StyleSheet.create({
modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 },
closeBtn: { width: 40, height: 40, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 10, marginLeft: 5, letterSpacing: 0.5 },
pickerContainer: { borderWidth: 1.5, borderRadius: 24, overflow: 'hidden' },
pickerContainer: { borderWidth: 1.5, borderRadius: 24, overflow: 'hidden', width: '100%' },
groupHeader: { paddingVertical: 10, paddingHorizontal: 18 },
groupHeaderText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
pickerItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 18, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
@@ -488,7 +708,7 @@ const styles = StyleSheet.create({
modalFooter: { flexDirection: 'row', gap: 15, marginTop: 15 },
btnModalPri: { flex: 2, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center' },
btnModalSec: { flex: 1, height: 62, borderRadius: 22, justifyContent: 'center', alignItems: 'center', borderWidth: 1.5 },
deleteCard: { width: '85%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center' },
deleteCard: { width: '85%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1, alignSelf: 'center', marginBottom: 30 },
deleteIconBg: { width: 80, height: 80, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
deleteTitle: { fontSize: 22, fontWeight: '900', marginBottom: 12 },
deleteSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 },
@@ -496,4 +716,47 @@ const styles = StyleSheet.create({
deleteBtnCancel: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
deleteBtnText: { fontSize: 14, fontWeight: '900' },
// --- ESTILOS ESPECÍFICOS PARA O AVISO CENTRADO ---
alertOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(26, 54, 93, 0.8)',
paddingHorizontal: 20
},
alertCard: {
width: '85%',
borderRadius: 30,
padding: 30,
alignItems: 'center',
borderWidth: 1,
elevation: 10,
shadowColor: '#000',
shadowOpacity: 0.15,
shadowRadius: 10,
},
alertIconBg: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 15
},
alertTitle: {
fontSize: 20,
fontWeight: '900',
textAlign: 'center',
marginBottom: 5
},
alertEmoji: {
fontSize: 28,
marginBottom: 10
},
alertSubtitle: {
fontSize: 14,
textAlign: 'center',
lineHeight: 22,
fontWeight: '500'
},
});