ATUA
This commit is contained in:
@@ -57,18 +57,14 @@ const AlunoHome = memo(() => {
|
||||
const [estagioDetalhes, setEstagioDetalhes] = useState<any>(null);
|
||||
const [horariosEstagio, setHorariosEstagio] = useState<any[]>([]);
|
||||
|
||||
// 🟢 NOVOS ESTADOS PARA REFLETIR A DECISÃO DO TUTOR
|
||||
const [presencasPendentes, setPresencasPendentes] = useState<Record<string, boolean>>({});
|
||||
const [presencasAprovadas, setPresencasAprovadas] = useState<Record<string, boolean>>({});
|
||||
const [presencasRejeitadas, setPresencasRejeitadas] = useState<Record<string, boolean>>({});
|
||||
|
||||
const [faltas, setFaltas] = useState<Record<string, boolean>>({});
|
||||
const [sumarios, setSumarios] = useState<Record<string, string>>({});
|
||||
const [urlsJustificacao, setUrlsJustificacao] = useState<Record<string, string>>({});
|
||||
// 🟢 O NOVO MOTOR CENTRAL (Substitui as 5 variáveis antigas)
|
||||
const [registosDiarios, setRegistosDiarios] = useState<Record<string, any>>({});
|
||||
const [statsFaltas, setStatsFaltas] = useState({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
|
||||
|
||||
const [pdf, setPdf] = useState<any>(null);
|
||||
const [editandoSumario, setEditandoSumario] = useState(false);
|
||||
const [sumarioInput, setSumarioInput] = useState("");
|
||||
|
||||
const [isLoadingDB, setIsLoadingDB] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [isLocating, setIsLocating] = useState(false);
|
||||
@@ -88,10 +84,12 @@ const AlunoHome = memo(() => {
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: azulPetroleo,
|
||||
laranja: laranjaEPVC,
|
||||
amarelo: '#F59E0B', // Adicionado para as faltas por confirmar
|
||||
cinzento: '#94A3B8', // Adicionado para presenças por confirmar
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
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]);
|
||||
@@ -142,43 +140,32 @@ const AlunoHome = memo(() => {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// 🟢 SEPARAMOS AS PRESENÇAS PELO ESTADO DO TUTOR
|
||||
const pPendente: any = {}, pAprovada: any = {}, pRejeitada: any = {};
|
||||
const f: any = {}, s: any = {}, u: any = {};
|
||||
|
||||
let countJustificadas = 0;
|
||||
let countInjustificadas = 0;
|
||||
let countPresencasAprovadas = 0;
|
||||
const novosRegistos: Record<string, any> = {};
|
||||
let countJustificadas = 0, countInjustificadas = 0, countPresencasAprovadas = 0;
|
||||
|
||||
data?.forEach(item => {
|
||||
if (item.estado === 'presente') {
|
||||
// Guarda o estado para o calendário
|
||||
if (item.estado_tutor === 'pendente') pPendente[item.data] = true;
|
||||
else if (item.estado_tutor === 'aprovado') {
|
||||
pAprovada[item.data] = true;
|
||||
countPresencasAprovadas++; // Só soma se o tutor aprovou
|
||||
novosRegistos[item.data] = item;
|
||||
|
||||
if (item.estado === 'presente' && item.estado_tutor === 'aprovado') {
|
||||
countPresencasAprovadas++;
|
||||
} else if (item.estado === 'faltou') {
|
||||
if (item.estado_tutor === 'aprovado' && item.justificacao_url) {
|
||||
countJustificadas++;
|
||||
} else if (item.estado_tutor === 'aprovado' || item.estado_tutor === 'rejeitado') {
|
||||
countInjustificadas++;
|
||||
}
|
||||
}
|
||||
else if (item.estado_tutor === 'rejeitado') pRejeitada[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++;
|
||||
}
|
||||
});
|
||||
|
||||
setPresencasPendentes(pPendente);
|
||||
setPresencasAprovadas(pAprovada);
|
||||
setPresencasRejeitadas(pRejeitada);
|
||||
setFaltas(f);
|
||||
setSumarios(s);
|
||||
setUrlsJustificacao(u);
|
||||
setRegistosDiarios(novosRegistos);
|
||||
setStatsFaltas({ justificadas: countJustificadas, injustificadas: countInjustificadas, totalPresencas: countPresencasAprovadas });
|
||||
|
||||
// Garante que o input do sumário reflete o dia atual
|
||||
if (novosRegistos[selectedDate]) setSumarioInput(novosRegistos[selectedDate].sumario || "");
|
||||
else setSumarioInput("");
|
||||
|
||||
} else {
|
||||
setPresencasPendentes({}); setPresencasAprovadas({}); setPresencasRejeitadas({});
|
||||
setFaltas({}); setSumarios({}); setUrlsJustificacao({});
|
||||
setRegistosDiarios({});
|
||||
setStatsFaltas({ justificadas: 0, injustificadas: 0, totalPresencas: 0 });
|
||||
}
|
||||
|
||||
@@ -189,7 +176,7 @@ const AlunoHome = memo(() => {
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, []));
|
||||
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, [selectedDate]));
|
||||
|
||||
useEffect(() => {
|
||||
const estagiosSubscription = supabase
|
||||
@@ -212,7 +199,7 @@ const AlunoHome = memo(() => {
|
||||
setRefreshing(true);
|
||||
await fetchDadosSupabase(true);
|
||||
setRefreshing(false);
|
||||
}, []);
|
||||
}, [selectedDate]);
|
||||
|
||||
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
|
||||
|
||||
@@ -241,9 +228,31 @@ const AlunoHome = memo(() => {
|
||||
};
|
||||
}, [selectedDate, estagioDetalhes, hojeStr, feriadosMap, statusEstagio]);
|
||||
|
||||
// 🟢 FUNÇÃO AUXILIAR PARA SABER SE O DIA JÁ TEM REGISTO (PARA DESATIVAR BOTÕES)
|
||||
const isDiaMarcado = () => {
|
||||
return !!presencasAprovadas[selectedDate] || !!presencasPendentes[selectedDate] || !!presencasRejeitadas[selectedDate] || !!faltas[selectedDate];
|
||||
// Modificado para ver no "cofre" em vez das antigas variáveis
|
||||
const isDiaMarcado = () => !!registosDiarios[selectedDate];
|
||||
|
||||
const savePresencaData = async (payload: any, successMessage: string) => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error("Usuário não autenticado.");
|
||||
|
||||
const finalPayload = { ...payload, aluno_id: user.id, data: selectedDate };
|
||||
const existing = registosDiarios[selectedDate];
|
||||
|
||||
if (existing) {
|
||||
const { error } = await supabase.from('presencas').update(finalPayload).eq('id', existing.id);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { error } = await supabase.from('presencas').insert([finalPayload]);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
await fetchDadosSupabase(true);
|
||||
showAlert(successMessage, "success");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
showAlert(e.message || "Erro ao guardar registo.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handlePresencaClick = async () => {
|
||||
@@ -265,28 +274,29 @@ const AlunoHome = memo(() => {
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') throw new Error("Sem acesso ao GPS.");
|
||||
const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
await supabase.from('presencas').upsert({
|
||||
aluno_id: user?.id,
|
||||
data: selectedDate,
|
||||
estado: 'presente',
|
||||
lat: loc.coords.latitude,
|
||||
|
||||
await savePresencaData({
|
||||
estado: 'presente',
|
||||
lat: loc.coords.latitude,
|
||||
lng: loc.coords.longitude,
|
||||
estado_tutor: 'pendente' // 🟢 NOVO REGISTO ENTRA COMO PENDENTE
|
||||
});
|
||||
showAlert("Presença marcada! A aguardar aprovação.", "success");
|
||||
} catch (e: any) { showAlert(e.message, "error"); }
|
||||
finally { setIsLocating(false); }
|
||||
estado_tutor: 'pendente'
|
||||
}, "Presença marcada! A aguardar aprovação da empresa.");
|
||||
|
||||
} catch (e: any) {
|
||||
showAlert(e.message, "error");
|
||||
} finally {
|
||||
setIsLocating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFalta = async () => {
|
||||
if (infoData.foraDeRange) return showAlert("Data fora do período de estágio.", "error");
|
||||
if (!infoData.valida) return showAlert("Não é possível registar falta hoje.", "error");
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' });
|
||||
showAlert("Falta registada.", "info");
|
||||
} catch (e) { showAlert("Erro ao registar falta.", "error"); }
|
||||
|
||||
await savePresencaData({
|
||||
estado: 'faltou',
|
||||
estado_tutor: 'pendente'
|
||||
}, "Falta registada e enviada para a entidade.");
|
||||
};
|
||||
|
||||
const selecionarDocumento = async () => {
|
||||
@@ -303,21 +313,72 @@ const AlunoHome = memo(() => {
|
||||
const fileName = `${user?.id}/${selectedDate}_justificacao.pdf`;
|
||||
const { error: uploadError } = await supabase.storage.from('justificacoes').upload(fileName, decode(fileBase64), { contentType: 'application/pdf', upsert: true });
|
||||
if (uploadError) throw uploadError;
|
||||
|
||||
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
|
||||
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
|
||||
|
||||
await savePresencaData({
|
||||
justificacao_url: publicUrl,
|
||||
estado_tutor: 'pendente'
|
||||
}, "Justificativo enviado à entidade com sucesso!");
|
||||
|
||||
setPdf(null);
|
||||
showAlert("Enviado com sucesso!", "success");
|
||||
} catch (e) { showAlert("Erro no upload.", "error"); }
|
||||
finally { setIsUploading(false); }
|
||||
} catch (e) {
|
||||
showAlert("Erro no upload do documento.", "error");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const guardarSumario = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
await supabase.from('presencas').update({ sumario: sumarios[selectedDate] }).match({ aluno_id: user?.id, data: selectedDate });
|
||||
setEditandoSumario(false);
|
||||
showAlert("Sumário guardado!", "success");
|
||||
} catch (e) { showAlert("Erro ao guardar.", "error"); }
|
||||
await savePresencaData({
|
||||
sumario: sumarioInput,
|
||||
estado_tutor: 'pendente'
|
||||
}, "Sumário submetido para validação!");
|
||||
setEditandoSumario(false);
|
||||
};
|
||||
|
||||
// 🟢 AS CORES AGORA REFLETEM AS TUAS REGRAS EXATAS
|
||||
const gerarMarcacoesCalendario = () => {
|
||||
const marcacoes: any = {};
|
||||
|
||||
// Feriados ficam com o azul principal da app
|
||||
Object.keys(feriadosMap).forEach(d => { marcacoes[d] = { marked: true, dotColor: '#000000b7' }; });
|
||||
|
||||
// Pontinhos Azuis: Identificar os dias que já passaram e que estão sem nada
|
||||
if (estagioDetalhes?.data_inicio) {
|
||||
const start = new Date(estagioDetalhes.data_inicio);
|
||||
const limit = new Date(hojeStr) < new Date(estagioDetalhes.data_fim) ? new Date(hojeStr) : new Date(estagioDetalhes.data_fim);
|
||||
|
||||
for (let d = new Date(start); d <= limit; d.setDate(d.getDate() + 1)) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
const diaSemana = d.getDay();
|
||||
if (diaSemana !== 0 && diaSemana !== 6 && !feriadosMap[dateStr]) {
|
||||
if (!registosDiarios[dateStr]) {
|
||||
marcacoes[dateStr] = { marked: true, dotColor: '#0947f1b7' }; // 🔵 Azul: Sem nada
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regras de Cores para dias com Registo
|
||||
Object.values(registosDiarios).forEach(reg => {
|
||||
let cor = themeStyles.azul;
|
||||
const temSumario = reg.sumario && reg.sumario.trim() !== '';
|
||||
|
||||
if (reg.estado === 'presente') {
|
||||
if (reg.estado_tutor === 'aprovado' && temSumario) cor = themeStyles.verde; // 🟢 Verde: Confirmada e com sumário
|
||||
else if (!temSumario) cor = themeStyles.amarelo; // 🟡 Amarelo: Presença sem sumário
|
||||
else cor = themeStyles.azul; // 🔵 Azul: Tem sumário mas falta validar ("Sem nada" da empresa)
|
||||
} else if (reg.estado === 'faltou') {
|
||||
if (reg.estado_tutor === 'aprovado') cor = themeStyles.cinzento; // 🔘 Cinzento: Falta confirmada
|
||||
else if (reg.justificacao_url) cor = themeStyles.amarelo; // 🟡 Amarelo: Falta justificada (ainda não aprovada)
|
||||
else cor = themeStyles.vermelho; // 🔴 Vermelho: Falta injustificada
|
||||
}
|
||||
marcacoes[reg.data] = { marked: true, dotColor: cor };
|
||||
});
|
||||
|
||||
marcacoes[selectedDate] = { ...marcacoes[selectedDate], selected: true, selectedColor: themeStyles.azul };
|
||||
return marcacoes;
|
||||
};
|
||||
|
||||
const getBadgeStyle = () => {
|
||||
@@ -325,26 +386,70 @@ const AlunoHome = memo(() => {
|
||||
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 badgeObj = getBadgeStyle();
|
||||
const horasTotais = estagioDetalhes?.horas_totais || 0;
|
||||
const horasConcluidas = estagioDetalhes?.horas_concluidas || 0;
|
||||
|
||||
const horasPorDia = Number(estagioDetalhes?.horas_diarias || 0);
|
||||
const horasConcluidas = statsFaltas.totalPresencas * horasPorDia;
|
||||
|
||||
const horasEmFalta = Math.max(0, horasTotais - horasConcluidas);
|
||||
|
||||
// 🟢 FUNÇÃO PARA MOSTRAR AVISO DE ESTADO DO DIA
|
||||
const renderAvisoEstadoDia = () => {
|
||||
if (presencasAprovadas[selectedDate]) {
|
||||
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.verde }}>✅ Horas validadas pela empresa</Text>;
|
||||
const reg = registosDiarios[selectedDate];
|
||||
|
||||
// Configuração base (Azul - Sem nada)
|
||||
let config = {
|
||||
icon: 'information-circle',
|
||||
cor: themeStyles.azul,
|
||||
bg: themeStyles.azulSuave,
|
||||
texto: 'Sem Registo (Sem Nada)'
|
||||
};
|
||||
|
||||
if (!reg) {
|
||||
// Se o dia não for válido para estágio ou for no futuro, não mostra nada
|
||||
if (infoData.foraDeRange || !infoData.valida || selectedDate > hojeStr) return null;
|
||||
// Se for um dia válido que já passou e está vazio, usa a config base (Azul)
|
||||
} else if (reg.estado === 'presente') {
|
||||
const temSumario = reg.sumario && reg.sumario.trim() !== '';
|
||||
|
||||
if (reg.estado_tutor === 'aprovado' && temSumario) {
|
||||
config = { icon: 'checkmark-circle', cor: themeStyles.verde, bg: themeStyles.verde + '20', texto: 'Presença Confirmada e com Sumário' };
|
||||
} else if (!temSumario) {
|
||||
config = { icon: 'warning', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Presença Sem Sumário' };
|
||||
} else {
|
||||
config = { icon: 'time', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Presença Pendente de Aprovação' };
|
||||
}
|
||||
} else {
|
||||
if (reg.estado_tutor === 'aprovado') {
|
||||
config = { icon: 'checkmark-done-circle', cor: themeStyles.cinzento, bg: themeStyles.cinzento + '20', texto: 'Falta Confirmada pela Entidade' };
|
||||
} else if (reg.justificacao_url) {
|
||||
config = { icon: 'document-text', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Falta Justificada (Em Análise)' };
|
||||
} else {
|
||||
config = { icon: 'close-circle', cor: themeStyles.vermelho, bg: themeStyles.vermelhoSuave, texto: 'Falta Injustificada' };
|
||||
}
|
||||
}
|
||||
if (presencasPendentes[selectedDate]) {
|
||||
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.laranja }}>⏳ A aguardar validação do tutor</Text>;
|
||||
}
|
||||
if (presencasRejeitadas[selectedDate]) {
|
||||
return <Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '700', color: themeStyles.vermelho }}>❌ O tutor rejeitou este registo. Corrige o sumário.</Text>;
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: config.bg,
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
marginTop: 15,
|
||||
borderWidth: 1,
|
||||
borderColor: config.cor + '40' // Adiciona um bocado de transparência à borda
|
||||
}}>
|
||||
<Ionicons name={config.icon as any} size={28} color={config.cor} />
|
||||
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: config.cor }}>
|
||||
{config.texto}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const regSelecionado = registosDiarios[selectedDate];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
@@ -353,14 +458,14 @@ const AlunoHome = memo(() => {
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.locationModal, { backgroundColor: themeStyles.card }]}>
|
||||
<View style={styles.modalHandle} />
|
||||
<View style={[styles.iconCircle, { backgroundColor: azulPetroleo + '15' }]}>
|
||||
<Ionicons name="location" size={40} color={azulPetroleo} />
|
||||
<View style={[styles.iconCircle, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Ionicons name="location" size={40} color={themeStyles.azul} />
|
||||
</View>
|
||||
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Confirmar Local</Text>
|
||||
<Text style={[styles.modalDesc, { color: themeStyles.textoSecundario }]}>
|
||||
Precisamos de validar a tua localização para confirmar que estás no estágio.
|
||||
</Text>
|
||||
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: azulPetroleo }]} onPress={executarMarcacao}>
|
||||
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: themeStyles.azul }]} onPress={executarMarcacao}>
|
||||
<Text style={styles.txtBtn}>Confirmar e Marcar</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.btnFechar} onPress={() => setShowLocationModal(false)}>
|
||||
@@ -371,7 +476,7 @@ const AlunoHome = memo(() => {
|
||||
</Modal>
|
||||
|
||||
{alertConfig && (
|
||||
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? '#EF4444' : azulPetroleo }]}>
|
||||
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? themeStyles.vermelho : themeStyles.azul }]}>
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
@@ -380,9 +485,7 @@ const AlunoHome = memo(() => {
|
||||
ref={scrollViewRef}
|
||||
contentContainerStyle={styles.container}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[themeStyles.azul]} tintColor={themeStyles.azul} />
|
||||
}
|
||||
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>
|
||||
@@ -396,80 +499,40 @@ const AlunoHome = memo(() => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 🚀 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}
|
||||
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>
|
||||
<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}
|
||||
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>
|
||||
<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}
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -478,17 +541,17 @@ const AlunoHome = memo(() => {
|
||||
|
||||
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda, marginTop: 4 }]} />
|
||||
|
||||
{/* CONTEÚDO 1: HORAS */}
|
||||
{activeTab === 'horas' && (
|
||||
<View>
|
||||
<View style={styles.dashGrid}>
|
||||
<View style={styles.dashGrid}>
|
||||
<View style={styles.dashGridItem}>
|
||||
<Text style={[styles.dashStatLabel, { color: themeStyles.textoSecundario }]}>REALIZADAS</Text>
|
||||
{/* 🟢 AGORA USA A VARIÁVEL CALCULADA */}
|
||||
<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>
|
||||
{/* 🟢 AGORA USA A VARIÁVEL CALCULADA */}
|
||||
<Text style={[styles.dashStatValue, { color: themeStyles.laranja }]}>{horasEmFalta}h</Text>
|
||||
</View>
|
||||
<View style={styles.dashDividerVertical} />
|
||||
@@ -496,8 +559,7 @@ const AlunoHome = memo(() => {
|
||||
<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}>
|
||||
@@ -519,7 +581,6 @@ const AlunoHome = memo(() => {
|
||||
</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 }} />
|
||||
@@ -528,11 +589,9 @@ const AlunoHome = memo(() => {
|
||||
{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 }}>
|
||||
@@ -549,10 +608,8 @@ const AlunoHome = memo(() => {
|
||||
</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 }}>
|
||||
@@ -569,7 +626,6 @@ const AlunoHome = memo(() => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Datas */}
|
||||
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />
|
||||
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
@@ -598,7 +654,7 @@ const AlunoHome = memo(() => {
|
||||
|
||||
<View style={styles.botoesLinha}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || isDiaMarcado()) && styles.disabled]}
|
||||
style={[styles.btn, { backgroundColor: themeStyles.laranja }, (!infoData.podeMarcar || isDiaMarcado()) && styles.disabled]}
|
||||
onPress={handlePresencaClick}
|
||||
disabled={!infoData.podeMarcar || isDiaMarcado() || isLocating}
|
||||
>
|
||||
@@ -618,18 +674,14 @@ const AlunoHome = memo(() => {
|
||||
key={isDarkMode ? 'dark' : 'light'}
|
||||
theme={{
|
||||
calendarBackground: themeStyles.card, dayTextColor: themeStyles.texto, monthTextColor: themeStyles.texto,
|
||||
todayTextColor: azulPetroleo, selectedDayBackgroundColor: azulPetroleo, textDisabledColor: isDarkMode ? '#333' : '#DDD'
|
||||
todayTextColor: themeStyles.azul, selectedDayBackgroundColor: themeStyles.azul, textDisabledColor: isDarkMode ? '#333' : '#DDD'
|
||||
}}
|
||||
markedDates={{
|
||||
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
|
||||
// 🟢 AS CORES AGORA REFLETEM O ESTADO DO TUTOR
|
||||
...Object.keys(presencasAprovadas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.verde } }), {}),
|
||||
...Object.keys(presencasPendentes).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.laranja } }), {}),
|
||||
...Object.keys(presencasRejeitadas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
|
||||
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: themeStyles.vermelho } }), {}),
|
||||
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
|
||||
markedDates={gerarMarcacoesCalendario()}
|
||||
onDayPress={(day) => {
|
||||
setSelectedDate(day.dateString);
|
||||
setEditandoSumario(false);
|
||||
setSumarioInput(registosDiarios[day.dateString]?.sumario || "");
|
||||
}}
|
||||
onDayPress={(day) => setSelectedDate(day.dateString)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -640,40 +692,41 @@ const AlunoHome = memo(() => {
|
||||
{/* 🟢 MOSTRA O AVISO DO ESTADO DO TUTOR */}
|
||||
{renderAvisoEstadoDia()}
|
||||
|
||||
{/* SE O ALUNO ESTIVER PRESENTE (Pendente ou Aprovado), MOSTRA O SUMÁRIO */}
|
||||
{(presencasPendentes[selectedDate] || presencasAprovadas[selectedDate] || presencasRejeitadas[selectedDate]) && (
|
||||
{/* SE O ALUNO ESTIVER PRESENTE, MOSTRA O SUMÁRIO */}
|
||||
{regSelecionado?.estado === 'presente' && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.rowTitle}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
|
||||
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={themeStyles.azul} /></TouchableOpacity>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto }]}
|
||||
multiline editable={editandoSumario} value={sumarios[selectedDate]}
|
||||
onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})}
|
||||
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto, backgroundColor: themeStyles.fundo }]}
|
||||
multiline editable={editandoSumario} value={sumarioInput}
|
||||
onChangeText={setSumarioInput}
|
||||
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>}
|
||||
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Submeter para Validação</Text></TouchableOpacity>}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{faltas[selectedDate] && (
|
||||
{/* SE O ALUNO FALTOU, MOSTRA O UPLOAD DE JUSTIFICAÇÃO */}
|
||||
{regSelecionado?.estado === 'faltou' && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar</Text>
|
||||
{urlsJustificacao[selectedDate] ? (
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar Falta</Text>
|
||||
{regSelecionado.justificacao_url ? (
|
||||
<View style={styles.justificadoBox}>
|
||||
<Ionicons name="checkmark-circle" size={20} color={themeStyles.verde} />
|
||||
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Justificativo Enviado</Text>
|
||||
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Justificativo Enviado à Entidade</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity style={[styles.btnUpload, { borderColor: azulPetroleo }]} onPress={selecionarDocumento}>
|
||||
<Ionicons name="document-attach-outline" size={20} color={azulPetroleo} />
|
||||
<Text style={{ color: azulPetroleo, fontWeight: '600' }}>{pdf ? pdf.name : "Selecionar PDF"}</Text>
|
||||
<TouchableOpacity style={[styles.btnUpload, { borderColor: themeStyles.azul }]} onPress={selecionarDocumento}>
|
||||
<Ionicons name="document-attach-outline" size={20} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '600' }}>{pdf ? pdf.name : "Selecionar Documento PDF"}</Text>
|
||||
</TouchableOpacity>
|
||||
{pdf && (
|
||||
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: azulPetroleo }]} onPress={enviarJustificativo} disabled={isUploading}>
|
||||
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter</Text>}
|
||||
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.azul }]} onPress={enviarJustificativo} disabled={isUploading}>
|
||||
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Documento</Text>}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
@@ -719,6 +772,7 @@ const styles = StyleSheet.create({
|
||||
alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' },
|
||||
avisoBox: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 18, borderRadius: 18, marginBottom: 20 },
|
||||
avisoTexto: { fontSize: 14, fontWeight: '700', flex: 1, lineHeight: 20 },
|
||||
avisoTxt: { textAlign: 'center', marginTop: 15, fontWeight: '800', fontSize: 15 },
|
||||
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 },
|
||||
@@ -727,7 +781,7 @@ const styles = StyleSheet.create({
|
||||
card: { padding: 20, borderRadius: 25, marginTop: 20, borderWidth: 1 },
|
||||
cardTitulo: { fontSize: 18, fontWeight: '700' },
|
||||
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 15 },
|
||||
input: { borderWidth: 1, borderRadius: 15, padding: 15, height: 100, textAlignVertical: 'top' },
|
||||
input: { borderWidth: 1, borderRadius: 15, padding: 15, height: 100, textAlignVertical: 'top', fontSize: 14, fontWeight: '600' },
|
||||
btnSalvar: { padding: 15, borderRadius: 15, marginTop: 15, alignItems: 'center' },
|
||||
btnUpload: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 15, borderRadius: 15, borderWidth: 2, borderStyle: 'dashed' },
|
||||
justificadoBox: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 10 },
|
||||
|
||||
@@ -1,56 +1,70 @@
|
||||
// app/Empresa/EmpresaHome.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
Linking,
|
||||
Modal,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
// Tipos de Ecrã possíveis neste "Tudo-em-Um"
|
||||
type AppScreen = 'DASHBOARD' | 'ALUNOS' | 'PEDIDOS_LISTA' | 'PEDIDOS_HISTORICO' | 'AVALIACOES' | 'DEFINICOES';
|
||||
|
||||
export default function EmpresaHome() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const [pendentes, setPendentes] = useState<any[]>([]);
|
||||
// ESTADOS DE NAVEGAÇÃO E DADOS
|
||||
const [currentScreen, setCurrentScreen] = useState<AppScreen>('DASHBOARD');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [empresaNome, setEmpresaNome] = useState('');
|
||||
|
||||
const [empresaData, setEmpresaData] = useState<any>(null);
|
||||
const [listaAlunos, setListaAlunos] = useState<any[]>([]);
|
||||
const [presencasGerais, setPresencasGerais] = useState<any[]>([]);
|
||||
|
||||
// ESTADOS PARA MODAIS E SELEÇÕES
|
||||
const [alunoSelecionado, setAlunoSelecionado] = useState<any>(null);
|
||||
const [modalDetalhesAluno, setModalDetalhesAluno] = useState(false);
|
||||
|
||||
// Estados do Toast Animado
|
||||
// TOAST ANIMADO
|
||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'error' | 'success' | 'info' }>({ visible: false, message: '', type: 'info' });
|
||||
const slideAnim = useRef(new Animated.Value(-100)).current;
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const themeStyles = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azul: '#2390a6',
|
||||
laranja: '#E38E00',
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
azulSuave: '#00c3ff',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FEE2E2',
|
||||
inputFundo: isDarkMode ? '#252525' : '#FBFDFF',
|
||||
aviso: isDarkMode ? '#2D2200' : '#FFF9E6',
|
||||
avisoTexto: isDarkMode ? '#FFD700' : '#856404'
|
||||
}), [isDarkMode]);
|
||||
|
||||
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
|
||||
setToast({ visible: true, message, type });
|
||||
Animated.timing(slideAnim, { toValue: 20, duration: 300, useNativeDriver: true }).start(() => {
|
||||
Animated.timing(slideAnim, { toValue: Platform.OS === 'ios' ? 50 : 20, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setTimeout(() => {
|
||||
Animated.timing(slideAnim, { toValue: -100, duration: 300, useNativeDriver: true })
|
||||
.start(() => setToast({ visible: false, message: '', type: 'info' }));
|
||||
@@ -58,93 +72,82 @@ export default function EmpresaHome() {
|
||||
});
|
||||
}, [slideAnim]);
|
||||
|
||||
const fetchValidaçõesPendentes = async (isManualRefresh = false) => {
|
||||
// 🚀 BUSCAR TUDO DE UMA VEZ
|
||||
const fetchTudo = async (isManualRefresh = false) => {
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
// 1. Identificar a empresa logada
|
||||
const { data: empresa, error: empError } = await supabase
|
||||
.from('empresas')
|
||||
.select('id, nome')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (empError || !empresa) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
// 1. Empresa
|
||||
const { data: empresa } = await supabase.from('empresas').select('*').eq('user_id', user.id).maybeSingle();
|
||||
if (!empresa) {
|
||||
setLoading(false);
|
||||
return Alert.alert("Erro", "Conta não associada a nenhuma empresa.");
|
||||
}
|
||||
setEmpresaNome(empresa.nome);
|
||||
setEmpresaData(empresa);
|
||||
|
||||
// 2. Buscar alunos vinculados (estágios)
|
||||
const { data: estagios } = await supabase
|
||||
.from('estagios')
|
||||
.select('aluno_id')
|
||||
.eq('empresa_id', empresa.id);
|
||||
|
||||
if (!estagios || estagios.length === 0) {
|
||||
setPendentes([]);
|
||||
return;
|
||||
}
|
||||
// 2. Alunos
|
||||
const { data: estagios } = await supabase.from('estagios').select('aluno_id').eq('empresa_id', empresa.id);
|
||||
const alunoIds = estagios?.map(e => e.aluno_id) || [];
|
||||
|
||||
const alunoIds = estagios.map(e => e.aluno_id);
|
||||
if (alunoIds.length > 0) {
|
||||
const { data: alunos } = await supabase.from('alunos').select('*').in('id', alunoIds);
|
||||
setListaAlunos(alunos || []);
|
||||
|
||||
// 3. Buscar nomes dos alunos
|
||||
const { data: alunos } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome')
|
||||
.in('id', alunoIds);
|
||||
// 3. Presenças e Faltas
|
||||
const { data: presencas } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.in('aluno_id', alunoIds)
|
||||
.order('data', { ascending: false });
|
||||
|
||||
setPresencasGerais(presencas || []);
|
||||
} else {
|
||||
setListaAlunos([]);
|
||||
setPresencasGerais([]);
|
||||
}
|
||||
|
||||
const mapaAlunos: Record<string, string> = {};
|
||||
alunos?.forEach(a => { mapaAlunos[a.id] = a.nome; });
|
||||
|
||||
// 4. Buscar apenas PRESENÇAS pendentes de validação
|
||||
// IMPORTANTE: Faltas não aparecem aqui, vão direto para o professor.
|
||||
const { data: presencas } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.in('aluno_id', alunoIds)
|
||||
.eq('estado', 'presente')
|
||||
.eq('estado_tutor', 'pendente')
|
||||
.order('data', { ascending: false });
|
||||
|
||||
const listaFormatada = presencas?.map(p => ({
|
||||
...p,
|
||||
aluno_nome: mapaAlunos[p.aluno_id] || 'Aluno Desconhecido'
|
||||
})) || [];
|
||||
|
||||
setPendentes(listaFormatada);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast("Falha ao carregar validações.", "error");
|
||||
showToast("Erro ao carregar dados", "error");
|
||||
} finally {
|
||||
if (!isManualRefresh) setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(useCallback(() => { fetchValidaçõesPendentes(); }, []));
|
||||
useFocusEffect(useCallback(() => { fetchTudo(); }, []));
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchValidaçõesPendentes(true);
|
||||
fetchTudo(true);
|
||||
}, []);
|
||||
|
||||
// 🟢 APROVAR OU RECUSAR (COM VERIFICAÇÃO ANTI-MENTIRAS DO SUPABASE)
|
||||
const lidarComPresenca = async (id: string, decisao: 'aprovado' | 'rejeitado') => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('presencas')
|
||||
.update({ estado_tutor: decisao })
|
||||
.eq('id', id);
|
||||
if (decisao === 'rejeitado') {
|
||||
// Tenta apagar e pede confirmação de volta (.select)
|
||||
const { data, error } = await supabase.from('presencas').delete().eq('id', id).select();
|
||||
if (error) throw error;
|
||||
if (!data || data.length === 0) throw new Error("A Base de Dados bloqueou a ação (Verifica se desligaste o RLS no Supabase)!");
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
showToast(decisao === 'aprovado' ? "Registo aprovado!" : "Registo rejeitado.", decisao === 'aprovado' ? 'success' : 'info');
|
||||
setPendentes(prev => prev.filter(p => p.id !== id));
|
||||
showToast("Registo recusado e apagado!", "info");
|
||||
// Remove da lista local da empresa
|
||||
setPresencasGerais(prev => prev.filter(p => p.id !== id));
|
||||
} else {
|
||||
// Tenta atualizar e pede confirmação de volta (.select)
|
||||
const { data, error } = await supabase.from('presencas').update({ estado_tutor: decisao }).eq('id', id).select();
|
||||
if (error) throw error;
|
||||
if (!data || data.length === 0) throw new Error("A Base de Dados bloqueou a ação (Verifica se desligaste o RLS no Supabase)!");
|
||||
|
||||
showToast("Validado com sucesso!", "success");
|
||||
// Atualiza a lista local da empresa
|
||||
setPresencasGerais(prev => prev.map(p => p.id === id ? { ...p, estado_tutor: decisao } : p));
|
||||
}
|
||||
} catch (e: any) {
|
||||
showToast("Erro ao processar validação.", "error");
|
||||
Alert.alert("Erro a Validar", e.message || "Não foi possível alterar o registo na Base de Dados.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,11 +157,225 @@ export default function EmpresaHome() {
|
||||
return parts.length !== 3 ? dataStr : `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// COMPONENTES DOS DIFERENTES ECRÃS (VIEWS)
|
||||
// ==========================================
|
||||
|
||||
const renderDashboard = () => (
|
||||
<View style={styles.grid}>
|
||||
{/* CARD 1: ALUNOS */}
|
||||
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('ALUNOS')}>
|
||||
<View style={[styles.dashIcon, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Ionicons name="people" size={32} color={themeStyles.azul} />
|
||||
</View>
|
||||
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Alunos</Text>
|
||||
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Verificar estagiários e detalhes.</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* CARD 2: PEDIDOS */}
|
||||
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('PEDIDOS_LISTA')}>
|
||||
<View style={[styles.dashIcon, { backgroundColor: themeStyles.laranja + '20' }]}>
|
||||
<Ionicons name="documents" size={32} color={themeStyles.laranja} />
|
||||
{presencasGerais.filter(p => p.estado_tutor === 'pendente').length > 0 && (
|
||||
<View style={styles.badgeNotif} />
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Pedidos</Text>
|
||||
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Validar sumários e faltas.</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* CARD 3: AVALIAÇÕES */}
|
||||
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('AVALIACOES')}>
|
||||
<View style={[styles.dashIcon, { backgroundColor: themeStyles.verde + '20' }]}>
|
||||
<Ionicons name="star" size={32} color={themeStyles.verde} />
|
||||
</View>
|
||||
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Avaliações</Text>
|
||||
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Em breve.</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* CARD 4: DEFINIÇÕES */}
|
||||
<TouchableOpacity style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]} onPress={() => setCurrentScreen('DEFINICOES')}>
|
||||
<View style={[styles.dashIcon, { backgroundColor: themeStyles.secundario + '20' }]}>
|
||||
<Ionicons name="settings" size={32} color={themeStyles.secundario} />
|
||||
</View>
|
||||
<Text style={[styles.dashTitle, { color: themeStyles.texto }]}>Definições</Text>
|
||||
<Text style={[styles.dashDesc, { color: themeStyles.secundario }]}>Gerir conta da empresa.</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderAlunos = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
|
||||
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar ao Menu</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Estagiários ({listaAlunos.length})</Text>
|
||||
|
||||
{listaAlunos.length === 0 ? (
|
||||
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 40 }}>Nenhum aluno associado a esta empresa.</Text>
|
||||
) : (
|
||||
listaAlunos.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
style={[styles.listCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
|
||||
onPress={() => { setAlunoSelecionado(aluno); setModalDetalhesAluno(true); }}
|
||||
>
|
||||
<View style={[styles.avatar, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: themeStyles.azul }]}>{aluno.nome.charAt(0)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<Text style={[styles.listCardTitle, { color: themeStyles.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.listCardSub, { color: themeStyles.secundario }]}>Toque para ver detalhes</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color={themeStyles.secundario} />
|
||||
</TouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderPedidosLista = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
|
||||
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar ao Menu</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Validar Registos</Text>
|
||||
<Text style={{ color: themeStyles.secundario, marginBottom: 20 }}>Selecione um aluno para ver o histórico e validar pedidos pendentes.</Text>
|
||||
|
||||
{listaAlunos.map(aluno => {
|
||||
const pendentesDoAluno = presencasGerais.filter(p => p.aluno_id === aluno.id && p.estado_tutor === 'pendente').length;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
style={[styles.listCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
|
||||
onPress={() => { setAlunoSelecionado(aluno); setCurrentScreen('PEDIDOS_HISTORICO'); }}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.listCardTitle, { color: themeStyles.texto }]}>{aluno.nome}</Text>
|
||||
</View>
|
||||
{pendentesDoAluno > 0 ? (
|
||||
<View style={[styles.badgeCount, { backgroundColor: themeStyles.vermelho }]}>
|
||||
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 12 }}>{pendentesDoAluno} PENDENTES</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Ionicons name="checkmark-circle" size={24} color={themeStyles.verde} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderPedidosHistorico = () => {
|
||||
if (!alunoSelecionado) return null;
|
||||
const historicoAluno = presencasGerais.filter(p => p.aluno_id === alunoSelecionado.id);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('PEDIDOS_LISTA')}>
|
||||
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar à Lista</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.pageTitle, { color: themeStyles.texto, fontSize: 20 }]}>Histórico: {alunoSelecionado.nome}</Text>
|
||||
|
||||
{historicoAluno.length === 0 ? (
|
||||
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 40 }}>Nenhum registo efetuado por este aluno.</Text>
|
||||
) : (
|
||||
historicoAluno.map(item => (
|
||||
<View key={item.id} style={[styles.historyCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.historyTop}>
|
||||
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>{formatarData(item.data)}</Text>
|
||||
<View style={[styles.statusTag, { backgroundColor: item.estado === 'faltou' ? themeStyles.vermelho + '15' : themeStyles.verde + '15' }]}>
|
||||
<Text style={[styles.statusTagText, { color: item.estado === 'faltou' ? themeStyles.vermelho : themeStyles.verde }]}>
|
||||
{item.estado === 'faltou' ? 'FALTA' : 'PRESENÇA'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.historyBody, { backgroundColor: themeStyles.fundo }]}>
|
||||
<Text style={{ color: themeStyles.texto, fontWeight: '600' }}>
|
||||
{item.estado === 'presente' ? (item.sumario || "Sem sumário registado.") : "Aluno marcou ausência."}
|
||||
</Text>
|
||||
{item.justificacao_url && (
|
||||
<TouchableOpacity style={{ marginTop: 10, flexDirection: 'row', alignItems: 'center' }} onPress={() => Linking.openURL(item.justificacao_url)}>
|
||||
<Ionicons name="document-attach" size={16} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '700', marginLeft: 5 }}>Ver Justificativo PDF</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* SÓ MOSTRA BOTÕES SE ESTIVER PENDENTE */}
|
||||
{item.estado_tutor === 'pendente' ? (
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]} onPress={() => lidarComPresenca(item.id, 'rejeitado')}>
|
||||
<Ionicons name="trash-outline" size={20} color={themeStyles.vermelho} />
|
||||
<Text style={{ color: themeStyles.vermelho, fontWeight: '800', marginLeft: 5 }}>Recusar (Apagar)</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btnAction, { backgroundColor: themeStyles.verde }]} onPress={() => lidarComPresenca(item.id, 'aprovado')}>
|
||||
<Ionicons name="checkmark" size={20} color="#fff" />
|
||||
<Text style={{ color: '#fff', fontWeight: '800', marginLeft: 5 }}>Aprovar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={{ textAlign: 'center', marginTop: 15, fontWeight: '800', color: themeStyles.verde }}>
|
||||
✅ Aprovado
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAvaliacoes = () => (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<TouchableOpacity style={[styles.btnVoltar, { position: 'absolute', top: 0, left: 0 }]} onPress={() => setCurrentScreen('DASHBOARD')}>
|
||||
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
<Ionicons name="construct" size={80} color={themeStyles.secundario} style={{ opacity: 0.3 }} />
|
||||
<Text style={{ fontSize: 24, fontWeight: '900', color: themeStyles.texto, marginTop: 20 }}>BREVEMENTE</Text>
|
||||
<Text style={{ color: themeStyles.secundario, textAlign: 'center', marginTop: 10 }}>A área de avaliações finais está em construção.</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderDefinicoes = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => setCurrentScreen('DASHBOARD')}>
|
||||
<Ionicons name="chevron-back" size={24} color={themeStyles.azul} />
|
||||
<Text style={{ color: themeStyles.azul, fontWeight: '700', fontSize: 16 }}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.pageTitle, { color: themeStyles.texto }]}>Definições</Text>
|
||||
|
||||
<View style={[styles.dashCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, width: '100%', marginBottom: 20 }]}>
|
||||
<Text style={{ color: themeStyles.secundario, fontWeight: '800', fontSize: 12, textTransform: 'uppercase' }}>Nome da Empresa</Text>
|
||||
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18, marginTop: 5 }}>{empresaData?.nome || 'N/A'}</Text>
|
||||
|
||||
<Text style={{ color: themeStyles.secundario, fontWeight: '800', fontSize: 12, textTransform: 'uppercase', marginTop: 15 }}>Tutor</Text>
|
||||
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18, marginTop: 5 }}>{empresaData?.tutor_nome || 'N/A'}</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={{ backgroundColor: themeStyles.vermelho, padding: 18, borderRadius: 16, alignItems: 'center', flexDirection: 'row', justifyContent: 'center' }}
|
||||
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
|
||||
>
|
||||
<Ionicons name="log-out" size={24} color="#fff" />
|
||||
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 16, marginLeft: 10 }}>Terminar Sessão</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* TOAST ANIMADO */}
|
||||
<Animated.View style={[
|
||||
styles.toastContainer,
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
@@ -170,138 +387,96 @@ export default function EmpresaHome() {
|
||||
<Text style={styles.toastText}>{toast.message}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.greeting, { color: themeStyles.secundario }]}>Bem-vindo,</Text>
|
||||
<Text style={[styles.title, { color: themeStyles.texto }]} numberOfLines={1}>
|
||||
{empresaNome || 'Entidade'}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.logoutBtn, { borderColor: themeStyles.borda, backgroundColor: themeStyles.card }]}
|
||||
onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}
|
||||
>
|
||||
<Ionicons name="log-out-outline" size={22} color={themeStyles.vermelho} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
|
||||
<View style={[styles.countBadge, { backgroundColor: themeStyles.laranja }]}>
|
||||
<Text style={styles.countText}>{pendentes.length}</Text>
|
||||
</View>
|
||||
<View style={styles.headerArea}>
|
||||
<Text style={[styles.appTitle, { color: themeStyles.texto }]}>Painel Empresa</Text>
|
||||
{empresaData?.nome ? <Text style={{ color: themeStyles.secundario, fontWeight: '700' }}>{empresaData.nome}</Text> : null}
|
||||
</View>
|
||||
|
||||
{loading && !refreshing ? (
|
||||
<View style={styles.centerBox}>
|
||||
<ActivityIndicator size="large" color={themeStyles.azul} />
|
||||
</View>
|
||||
<View style={styles.centerBox}><ActivityIndicator size="large" color={themeStyles.azul} /></View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={themeStyles.azul} />}
|
||||
>
|
||||
{pendentes.length === 0 ? (
|
||||
<View style={[styles.emptyBox, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={[styles.emptyIconCircle, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Ionicons name="shield-checkmark-outline" size={40} color={themeStyles.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyTitle, { color: themeStyles.texto }]}>Tudo em ordem!</Text>
|
||||
<Text style={[styles.emptyDesc, { color: themeStyles.secundario }]}>
|
||||
Não existem registos pendentes de validação. **Vai dar merda** se os alunos não trabalharem! 😂
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
pendentes.map((item) => (
|
||||
<View key={item.id} style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.cardTop}>
|
||||
<View style={[styles.avatar, { backgroundColor: themeStyles.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: themeStyles.azul }]}>{item.aluno_nome.charAt(0)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 12 }}>
|
||||
<Text style={[styles.alunoName, { color: themeStyles.texto }]} numberOfLines={1}>{item.aluno_nome}</Text>
|
||||
<View style={styles.dataRow}>
|
||||
<Ionicons name="calendar-outline" size={14} color={themeStyles.laranja} />
|
||||
<Text style={[styles.dataText, { color: themeStyles.secundario }]}>{formatarData(item.data)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.statusTag, { backgroundColor: themeStyles.laranja + '15' }]}>
|
||||
<Text style={[styles.statusTagText, { color: themeStyles.laranja }]}>PENDENTE</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.sumarioBox, { backgroundColor: themeStyles.fundo }]}>
|
||||
<Text style={[styles.sumarioLabel, { color: themeStyles.secundario }]}>SUMÁRIO DO DIA:</Text>
|
||||
<Text style={[styles.sumarioText, { color: themeStyles.texto }]}>
|
||||
{item.sumario || "O aluno não descreveu as atividades deste dia."}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.vermelhoSuave }]}
|
||||
onPress={() => lidarComPresenca(item.id, 'rejeitado')}
|
||||
>
|
||||
<Ionicons name="close-circle-outline" size={20} color={themeStyles.vermelho} />
|
||||
<Text style={[styles.btnActionText, { color: themeStyles.vermelho }]}>Rejeitar</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: themeStyles.verde }]}
|
||||
onPress={() => lidarComPresenca(item.id, 'aprovado')}
|
||||
>
|
||||
<Ionicons name="checkmark-circle-outline" size={20} color="#fff" />
|
||||
<Text style={[styles.btnActionText, { color: '#fff' }]}>Aprovar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
{currentScreen === 'DASHBOARD' && renderDashboard()}
|
||||
{currentScreen === 'ALUNOS' && renderAlunos()}
|
||||
{currentScreen === 'PEDIDOS_LISTA' && renderPedidosLista()}
|
||||
{currentScreen === 'PEDIDOS_HISTORICO' && renderPedidosHistorico()}
|
||||
{currentScreen === 'AVALIACOES' && renderAvaliacoes()}
|
||||
{currentScreen === 'DEFINICOES' && renderDefinicoes()}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* MODAL DETALHES DO ALUNO */}
|
||||
<Modal visible={modalDetalhesAluno} animationType="slide" transparent>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: themeStyles.card }]}>
|
||||
<View style={styles.modalHandle} />
|
||||
<Text style={[styles.pageTitle, { color: themeStyles.texto, textAlign: 'center' }]}>{alunoSelecionado?.nome}</Text>
|
||||
|
||||
<View style={[styles.infoBox, { backgroundColor: themeStyles.fundo }]}>
|
||||
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>Nº ESCOLA</Text>
|
||||
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18 }}>{alunoSelecionado?.n_escola || 'N/A'}</Text>
|
||||
</View>
|
||||
<View style={[styles.infoBox, { backgroundColor: themeStyles.fundo, marginTop: 10 }]}>
|
||||
<Text style={{ color: themeStyles.secundario, fontWeight: '800' }}>TURMA/CURSO</Text>
|
||||
<Text style={{ color: themeStyles.texto, fontWeight: '900', fontSize: 18 }}>{alunoSelecionado?.turma_curso || 'N/A'}</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={[styles.btnFecharModal, { backgroundColor: themeStyles.azul }]} onPress={() => setModalDetalhesAluno(false)}>
|
||||
<Text style={{ color: '#fff', fontWeight: '900', fontSize: 16 }}>Fechar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
|
||||
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
toastContainer: { position: 'absolute', left: 20, right: 20, zIndex: 9999, flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 16, elevation: 6 },
|
||||
toastText: { color: '#FFF', fontSize: 14, fontWeight: '700', marginLeft: 12 },
|
||||
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 20 },
|
||||
greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
title: { fontSize: 24, fontWeight: '900', marginTop: 2 },
|
||||
logoutBtn: { width: 48, height: 48, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, marginBottom: 15, gap: 10 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
countBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10 },
|
||||
countText: { color: '#fff', fontSize: 12, fontWeight: '900' },
|
||||
|
||||
scroll: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
headerArea: { paddingHorizontal: 20, paddingTop: 20, paddingBottom: 10 },
|
||||
appTitle: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 },
|
||||
scroll: { padding: 20, paddingBottom: 60 },
|
||||
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
emptyBox: { alignItems: 'center', padding: 40, borderRadius: 28, borderWidth: 1, borderStyle: 'dashed', marginTop: 20 },
|
||||
emptyIconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
emptyTitle: { fontSize: 20, fontWeight: '900', marginBottom: 8 },
|
||||
emptyDesc: { fontSize: 14, textAlign: 'center', lineHeight: 22, fontWeight: '600', opacity: 0.8 },
|
||||
// DASHBOARD GRID
|
||||
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
|
||||
dashCard: { width: '48%', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||
dashIcon: { width: 56, height: 56, borderRadius: 18, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
dashTitle: { fontSize: 18, fontWeight: '900', marginBottom: 5 },
|
||||
dashDesc: { fontSize: 12, fontWeight: '600', lineHeight: 18 },
|
||||
badgeNotif: { position: 'absolute', top: -5, right: -5, width: 14, height: 14, borderRadius: 7, backgroundColor: '#EF4444', borderWidth: 2, borderColor: '#fff' },
|
||||
|
||||
card: { padding: 20, borderRadius: 28, borderWidth: 1, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 10 },
|
||||
cardTop: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 },
|
||||
// SUB-PAGES
|
||||
btnVoltar: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
pageTitle: { fontSize: 24, fontWeight: '900', marginBottom: 20 },
|
||||
|
||||
// LISTAS
|
||||
listCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 20, borderWidth: 1, marginBottom: 12 },
|
||||
avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, fontWeight: '900' },
|
||||
alunoName: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
dataRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 },
|
||||
dataText: { fontSize: 13, fontWeight: '700' },
|
||||
statusTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8 },
|
||||
statusTagText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.5 },
|
||||
|
||||
sumarioBox: { padding: 16, borderRadius: 18, marginBottom: 18 },
|
||||
sumarioLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, letterSpacing: 0.5 },
|
||||
sumarioText: { fontSize: 14, fontWeight: '600', lineHeight: 22 },
|
||||
listCardTitle: { fontSize: 16, fontWeight: '800' },
|
||||
listCardSub: { fontSize: 12, fontWeight: '600', marginTop: 2 },
|
||||
badgeCount: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12 },
|
||||
|
||||
// HISTÓRICO
|
||||
historyCard: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
|
||||
historyTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
|
||||
statusTag: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
statusTagText: { fontSize: 10, fontWeight: '900', letterSpacing: 0.5 },
|
||||
historyBody: { padding: 15, borderRadius: 16, marginBottom: 15 },
|
||||
actionRow: { flexDirection: 'row', gap: 12 },
|
||||
btnAction: { flex: 1, flexDirection: 'row', height: 52, borderRadius: 16, justifyContent: 'center', alignItems: 'center', gap: 8 },
|
||||
btnActionText: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 }
|
||||
btnAction: { flex: 1, flexDirection: 'row', height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
// MODAL
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
|
||||
modalContent: { borderTopLeftRadius: 30, borderTopRightRadius: 30, padding: 30, alignItems: 'center', paddingBottom: 50 },
|
||||
modalHandle: { width: 50, height: 6, backgroundColor: '#cbd5e1', borderRadius: 10, marginBottom: 20 },
|
||||
infoBox: { width: '100%', padding: 15, borderRadius: 16, alignItems: 'center' },
|
||||
btnFecharModal: { width: '100%', padding: 18, borderRadius: 16, alignItems: 'center', marginTop: 20 }
|
||||
});
|
||||
@@ -51,7 +51,7 @@ export default function LoginScreen() {
|
||||
} else if (data.tipo === 'aluno') {
|
||||
router.replace('/Aluno/AlunoHome');
|
||||
} else if (data.tipo === 'empresa') {
|
||||
router.replace('/Empresas/EmpresaHome'); // 🟢 Rota da empresa adicionada!
|
||||
router.replace('/Empresa/EmpresaHome'); // 🟢 Rota da empresa adicionada!
|
||||
} else {
|
||||
Alert.alert('Erro', 'Tipo de conta inválido');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user