atualizacoes
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user