atualizacoess
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
// app/Aluno/AlunoHome.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { decode } from 'base64-arraybuffer';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
@@ -11,7 +10,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
Linking,
|
||||
Modal,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
@@ -26,6 +25,7 @@ import { Calendar, LocaleConfig } from 'react-native-calendars';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
// Configuração PT-PT para o Calendário
|
||||
LocaleConfig.locales['pt'] = {
|
||||
monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
|
||||
monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
|
||||
@@ -48,34 +48,22 @@ const AlunoHome = memo(() => {
|
||||
const router = useRouter();
|
||||
const hojeStr = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Estados de Dados
|
||||
const [selectedDate, setSelectedDate] = useState(hojeStr);
|
||||
const [configEstagio, setConfigEstagio] = useState({ inicio: '2026-01-05', fim: '2026-05-30' });
|
||||
const [configEstagio, setConfigEstagio] = useState({ inicio: '', fim: '' });
|
||||
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>>({});
|
||||
|
||||
// Estados de UI
|
||||
const [pdf, setPdf] = useState<any>(null);
|
||||
const [editandoSumario, setEditandoSumario] = useState(false);
|
||||
const [isLoadingDB, setIsLoadingDB] = useState(true);
|
||||
const [isLocating, setIsLocating] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// --- SISTEMA DE AVISOS MODERNOS ---
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
const showAlert = (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));
|
||||
};
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
const laranjaEPVC = '#dd8707';
|
||||
|
||||
@@ -90,76 +78,80 @@ const AlunoHome = memo(() => {
|
||||
verde: '#10B981',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
carregarConfigLocal();
|
||||
fetchDadosSupabase();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const carregarConfigLocal = async () => {
|
||||
try {
|
||||
const config = await AsyncStorage.getItem('@dados_estagio');
|
||||
if (config) setConfigEstagio(JSON.parse(config));
|
||||
} catch (e) { console.error(e); }
|
||||
const showAlert = (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));
|
||||
};
|
||||
|
||||
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 });
|
||||
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: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoadingDB(false);
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
finally { setIsLoadingDB(false); }
|
||||
};
|
||||
|
||||
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
|
||||
|
||||
const infoData = useMemo(() => {
|
||||
const data = new Date(selectedDate);
|
||||
const diaSemana = data.getDay();
|
||||
const diaSemana = data.getDay();
|
||||
const nomeFeriado = feriadosMap[selectedDate];
|
||||
const fora = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
|
||||
const antesDoInicio = configEstagio.inicio && selectedDate < configEstagio.inicio;
|
||||
const depoisDoFim = configEstagio.fim && selectedDate > configEstagio.fim;
|
||||
return {
|
||||
valida: diaSemana !== 0 && diaSemana !== 6 && !fora && !nomeFeriado,
|
||||
podeMarcar: selectedDate === hojeStr && !fora && !nomeFeriado,
|
||||
nomeFeriado
|
||||
valida: diaSemana !== 0 && diaSemana !== 6 && !antesDoInicio && !depoisDoFim && !nomeFeriado,
|
||||
podeMarcar: selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado,
|
||||
nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: antesDoInicio || depoisDoFim
|
||||
};
|
||||
}, [selectedDate, configEstagio, hojeStr, feriadosMap]);
|
||||
|
||||
const handlePresenca = async () => {
|
||||
if (!infoData.podeMarcar) return showAlert("Não podes marcar presença nesta data.", "error");
|
||||
const handlePresencaClick = async () => {
|
||||
const { status } = await Location.getForegroundPermissionsAsync();
|
||||
if (status === 'granted') {
|
||||
executarMarcacao();
|
||||
} else {
|
||||
setShowLocationModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const executarMarcacao = async () => {
|
||||
setShowLocationModal(false);
|
||||
setIsLocating(true);
|
||||
try {
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') throw new Error("Sem permissão de GPS.");
|
||||
const loc = await Location.getCurrentPositionAsync({});
|
||||
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();
|
||||
const { error } = await supabase.from('presencas').upsert({
|
||||
await supabase.from('presencas').upsert({
|
||||
aluno_id: user?.id, data: selectedDate, estado: 'presente', lat: loc.coords.latitude, lng: loc.coords.longitude
|
||||
});
|
||||
if (error) throw error;
|
||||
showAlert("Presença registada com sucesso!", "success");
|
||||
showAlert("Presença marcada com sucesso!", "success");
|
||||
fetchDadosSupabase();
|
||||
} catch (e: any) {
|
||||
showAlert(e.message, "error");
|
||||
} finally { setIsLocating(false); }
|
||||
} catch (e: any) { showAlert(e.message, "error"); }
|
||||
finally { setIsLocating(false); }
|
||||
};
|
||||
|
||||
const handleFalta = async () => {
|
||||
if (!infoData.valida) return showAlert("Data inválida para registar falta.", "error");
|
||||
if (infoData.foraDeRange) return showAlert("Fora do período de estágio.", "error");
|
||||
if (!infoData.valida) return showAlert("Não podes marcar falta neste dia.", "error");
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
await supabase.from('presencas').upsert({ aluno_id: user?.id, data: selectedDate, estado: 'faltou' });
|
||||
@@ -168,6 +160,29 @@ const AlunoHome = memo(() => {
|
||||
} catch (e) { showAlert("Erro ao registar falta.", "error"); }
|
||||
};
|
||||
|
||||
const selecionarDocumento = async () => {
|
||||
const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
|
||||
if (!res.canceled) setPdf(res.assets[0]);
|
||||
};
|
||||
|
||||
const enviarJustificativo = async () => {
|
||||
if (!pdf) return;
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const fileBase64 = await FileSystem.readAsStringAsync(pdf.uri, { encoding: FileSystem.EncodingType.Base64 });
|
||||
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 });
|
||||
setPdf(null);
|
||||
showAlert("Justificativo enviado!", "success");
|
||||
fetchDadosSupabase();
|
||||
} catch (e) { showAlert("Erro no upload.", "error"); }
|
||||
finally { setIsUploading(false); }
|
||||
};
|
||||
|
||||
const guardarSumario = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
@@ -178,59 +193,54 @@ const AlunoHome = memo(() => {
|
||||
} catch (e) { showAlert("Erro ao guardar sumário.", "error"); }
|
||||
};
|
||||
|
||||
const enviarJustificacao = async () => {
|
||||
if (!pdf) return;
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const fileName = `${user?.id}/${selectedDate}_${Date.now()}.pdf`;
|
||||
const base64 = await FileSystem.readAsStringAsync(pdf.uri, { encoding: 'base64' });
|
||||
const { error: upErr } = await supabase.storage.from('justificacoes').upload(fileName, decode(base64), { contentType: 'application/pdf' });
|
||||
if (upErr) throw upErr;
|
||||
|
||||
const { data: { publicUrl } } = supabase.storage.from('justificacoes').getPublicUrl(fileName);
|
||||
await supabase.from('presencas').update({ justificacao_url: publicUrl }).match({ aluno_id: user?.id, data: selectedDate });
|
||||
|
||||
showAlert("Documento enviado!", "success");
|
||||
setPdf(null);
|
||||
fetchDadosSupabase();
|
||||
} catch (e: any) { showAlert(e.message, "error"); }
|
||||
finally { setIsUploading(false); }
|
||||
};
|
||||
|
||||
const visualizarDocumento = async (url: string) => {
|
||||
const supported = await Linking.canOpenURL(url);
|
||||
if (supported) await Linking.openURL(url);
|
||||
else showAlert("Não foi possível abrir o ficheiro.", "error");
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
{/* ALERT OVERLAY MODERNO */}
|
||||
|
||||
<Modal visible={showLocationModal} transparent animationType="slide">
|
||||
<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>
|
||||
<Text style={[styles.modalTitle, { color: themeStyles.texto }]}>Validar Localização</Text>
|
||||
<Text style={[styles.modalDesc, { color: themeStyles.textoSecundario }]}>
|
||||
Para registar a tua presença, precisamos de confirmar que te encontras no local de estágio.
|
||||
</Text>
|
||||
<TouchableOpacity style={[styles.btnConfirmar, { backgroundColor: azulPetroleo }]} onPress={executarMarcacao}>
|
||||
<Text style={styles.txtBtn}>Confirmar e Marcar</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.btnFechar} onPress={() => setShowLocationModal(false)}>
|
||||
<Text style={[styles.txtFechar, { color: themeStyles.textoSecundario }]}>Agora não</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{alertConfig && (
|
||||
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? '#EF4444' : alertConfig.type === 'success' ? '#10B981' : azulPetroleo }]}>
|
||||
<Ionicons name={alertConfig.type === 'error' ? "alert-circle" : "checkmark-circle"} size={20} color="#fff" />
|
||||
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? '#EF4444' : azulPetroleo }]}>
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}>
|
||||
<Ionicons name="person-circle-outline" size={32} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: themeStyles.texto }]}>Estágios+</Text>
|
||||
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')}>
|
||||
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.topIcons}>
|
||||
<TouchableOpacity onPress={() => router.push('/Aluno/definicoes')} style={{ marginRight: 15 }}>
|
||||
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => router.push('/Aluno/perfil')}>
|
||||
<Ionicons name="person-circle-outline" size={30} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.botoesLinha}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, { backgroundColor: laranjaEPVC }, (!infoData.podeMarcar || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
onPress={handlePresenca}
|
||||
onPress={handlePresencaClick}
|
||||
disabled={!infoData.podeMarcar || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
|
||||
>
|
||||
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
|
||||
@@ -240,21 +250,22 @@ const AlunoHome = memo(() => {
|
||||
onPress={handleFalta}
|
||||
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
|
||||
>
|
||||
<Text style={styles.txtBtn}>Faltei</Text>
|
||||
<Text style={styles.txtBtn}>Marcar Falta</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
{isLoadingDB && <View style={styles.loaderOverlay}><ActivityIndicator size="large" color={azulPetroleo} /></View>}
|
||||
<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,
|
||||
arrowColor: azulPetroleo,
|
||||
selectedDayBackgroundColor: azulPetroleo
|
||||
selectedDayBackgroundColor: azulPetroleo,
|
||||
textDisabledColor: isDarkMode ? '#333' : '#DDD'
|
||||
}}
|
||||
markedDates={{
|
||||
...Object.keys(feriadosMap).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: azulPetroleo } }), {}),
|
||||
@@ -262,46 +273,48 @@ const AlunoHome = memo(() => {
|
||||
...Object.keys(faltas).reduce((acc, d) => ({ ...acc, [d]: { marked: true, dotColor: '#EF4444' } }), {}),
|
||||
[selectedDate]: { selected: true, selectedColor: azulPetroleo }
|
||||
}}
|
||||
onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }}
|
||||
onDayPress={(day) => setSelectedDate(day.dateString)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{presencas[selectedDate] && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.rowTitle}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário</Text>
|
||||
{!editandoSumario && <TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>}
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário do Dia</Text>
|
||||
<TouchableOpacity onPress={() => setEditandoSumario(true)}><Ionicons name="create-outline" size={22} color={azulPetroleo} /></TouchableOpacity>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto }]}
|
||||
multiline editable={editandoSumario}
|
||||
value={sumarios[selectedDate]}
|
||||
onChangeText={(txt) => setSumarios({...sumarios, [selectedDate]: txt})}
|
||||
placeholder="O que fizeste hoje?"
|
||||
placeholder="Descreve o que fizeste..."
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
{editandoSumario && <TouchableOpacity style={[styles.btnAcao, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar</Text></TouchableOpacity>}
|
||||
{editandoSumario && <TouchableOpacity style={[styles.btnSalvar, { backgroundColor: themeStyles.verde }]} onPress={guardarSumario}><Text style={styles.txtBtn}>Guardar Sumário</Text></TouchableOpacity>}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{faltas[selectedDate] && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificação</Text>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificar Falta</Text>
|
||||
{urlsJustificacao[selectedDate] ? (
|
||||
<TouchableOpacity style={[styles.btnVer, { backgroundColor: themeStyles.textoSecundario }]} onPress={() => visualizarDocumento(urlsJustificacao[selectedDate])}>
|
||||
<Ionicons name="document-text-outline" size={20} color="#fff" />
|
||||
<Text style={[styles.txtBtn, { marginLeft: 8 }]}>Ver PDF</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View>
|
||||
<TouchableOpacity style={[styles.btnAnexar, { borderColor: themeStyles.borda }]} onPress={async () => {
|
||||
const res = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
|
||||
if (!res.canceled) setPdf(res.assets[0]);
|
||||
}}>
|
||||
<Text style={{ color: themeStyles.texto }}>{pdf ? pdf.name : 'Selecionar PDF'}</Text>
|
||||
</TouchableOpacity>
|
||||
{pdf && <TouchableOpacity style={[styles.btnAcao, { backgroundColor: themeStyles.verde }]} onPress={enviarJustificacao} disabled={isUploading}>{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Enviar</Text>}</TouchableOpacity>}
|
||||
<View style={styles.justificadoBox}>
|
||||
<Ionicons name="checkmark-circle" size={20} color={themeStyles.verde} />
|
||||
<Text style={{ color: themeStyles.verde, fontWeight: '700' }}>Documento Enviado</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>
|
||||
{pdf && (
|
||||
<TouchableOpacity style={[styles.btnSalvar, { backgroundColor: azulPetroleo }]} onPress={enviarJustificativo} disabled={isUploading}>
|
||||
{isUploading ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Submeter Justificativo</Text>}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
@@ -312,24 +325,33 @@ const AlunoHome = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 999, elevation: 10 },
|
||||
alertText: { color: '#fff', fontWeight: 'bold', marginLeft: 10, flex: 1 },
|
||||
container: { padding: 20, paddingBottom: 40 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
title: { fontSize: 22, fontWeight: '800' },
|
||||
container: { padding: 20 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 25 },
|
||||
topIcons: { flexDirection: 'row', alignItems: 'center' },
|
||||
title: { fontSize: 26, fontWeight: '900' },
|
||||
alertBar: { position: 'absolute', top: 50, left: 20, right: 20, padding: 15, borderRadius: 15, zIndex: 1000 },
|
||||
alertText: { color: '#fff', fontWeight: 'bold', textAlign: 'center' },
|
||||
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
|
||||
btn: { padding: 16, borderRadius: 16, width: '48%', alignItems: 'center' },
|
||||
btnAcao: { padding: 14, borderRadius: 12, marginTop: 10, alignItems: 'center' },
|
||||
btnAnexar: { borderWidth: 1, padding: 14, borderRadius: 12, marginBottom: 10, alignItems: 'center', borderStyle: 'dashed' },
|
||||
btnVer: { padding: 14, borderRadius: 12, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
|
||||
btn: { padding: 18, borderRadius: 22, width: '48%', alignItems: 'center', elevation: 3 },
|
||||
txtBtn: { color: '#fff', fontWeight: '800', fontSize: 14 },
|
||||
disabled: { opacity: 0.4 },
|
||||
txtBtn: { color: '#fff', fontWeight: 'bold' },
|
||||
cardCalendar: { borderRadius: 24, padding: 10, borderWidth: 1, position: 'relative', overflow: 'hidden' },
|
||||
loaderOverlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.4)', zIndex: 10 },
|
||||
card: { padding: 20, borderRadius: 24, marginTop: 20, borderWidth: 1 },
|
||||
cardTitulo: { fontSize: 17, fontWeight: '700' },
|
||||
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
||||
input: { borderWidth: 1, borderRadius: 12, padding: 15, height: 100, textAlignVertical: 'top' }
|
||||
cardCalendar: { borderRadius: 30, padding: 10, borderWidth: 1, overflow: 'hidden' },
|
||||
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' },
|
||||
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 },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
|
||||
locationModal: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 30, alignItems: 'center', paddingBottom: 50 },
|
||||
modalHandle: { width: 50, height: 6, backgroundColor: '#cbd5e1', borderRadius: 10, marginBottom: 30 },
|
||||
iconCircle: { width: 90, height: 90, borderRadius: 45, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '800', marginBottom: 12 },
|
||||
modalDesc: { fontSize: 16, textAlign: 'center', lineHeight: 24, marginBottom: 35, paddingHorizontal: 15 },
|
||||
btnConfirmar: { width: '100%', padding: 18, borderRadius: 20, alignItems: 'center' },
|
||||
btnFechar: { marginTop: 20 },
|
||||
txtFechar: { fontWeight: '700', fontSize: 16 }
|
||||
});
|
||||
|
||||
export default AlunoHome;
|
||||
@@ -1,11 +1,11 @@
|
||||
// app/Definicoes.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useMemo, useState } from 'react'; // Importado useMemo e memo
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
Linking,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
@@ -13,149 +13,219 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
const Definicoes = memo(() => {
|
||||
const router = useRouter();
|
||||
const [notificacoes, setNotificacoes] = useState(true);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isDarkMode, toggleTheme } = useTheme();
|
||||
const [notificacoes, setNotificacoes] = useState(true);
|
||||
|
||||
// --- SISTEMA DE AVISOS MODERNOS ---
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
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(2500),
|
||||
Animated.timing(alertOpacity, { toValue: 0, duration: 300, useNativeDriver: true })
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, []);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
|
||||
// Otimização de cores para evitar lag no render
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#ffffff' : '#000000',
|
||||
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
|
||||
borda: isDarkMode ? '#333' : '#f1f3f5',
|
||||
sair: '#dc3545',
|
||||
azul: '#0d6efd'
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: 'rgba(239, 68, 68, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const handleLogout = () => {
|
||||
Alert.alert(
|
||||
"Terminar Sessão",
|
||||
"Tem a certeza que deseja sair da aplicação?",
|
||||
[
|
||||
{ text: "Cancelar", style: "cancel" },
|
||||
{
|
||||
text: "Sair",
|
||||
style: "destructive",
|
||||
onPress: () => router.replace('/')
|
||||
}
|
||||
]
|
||||
);
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
router.replace('/');
|
||||
} catch (e) {
|
||||
showAlert("Erro ao sair da conta", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const abrirEmail = () => Linking.openURL(`mailto:epvc@epvc.pt`);
|
||||
const abrirEmail2 = () => Linking.openURL(`mailto:secretaria@epvc.pt`);
|
||||
const abrirTelefone = () => Linking.openURL('tel:252 641 805');
|
||||
const abrirURL = (url: string) => {
|
||||
Linking.canOpenURL(url).then(supported => {
|
||||
if (supported) Linking.openURL(url);
|
||||
else showAlert("Não foi possível abrir o link", "error");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnVoltar, { backgroundColor: cores.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
|
||||
<Text style={[styles.subtituloSecao, { color: cores.azul }]}>Preferências</Text>
|
||||
|
||||
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name="notifications-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.texto }}>Notificações</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={notificacoes}
|
||||
onValueChange={setNotificacoes}
|
||||
trackColor={{ false: '#767577', true: cores.azul }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name={isDarkMode ? "moon" : "sunny-outline"} size={20} color={cores.texto} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.texto }}>Modo escuro</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={isDarkMode}
|
||||
onValueChange={toggleTheme} // Chamada direta otimizada pelo ThemeContext
|
||||
trackColor={{ false: '#767577', true: cores.azul }}
|
||||
thumbColor={isDarkMode ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.subtituloSecao, { color: cores.azul, marginTop: 25 }]}>Suporte e Contactos</Text>
|
||||
|
||||
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail}>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.texto }}>Direção</Text>
|
||||
</View>
|
||||
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>epvc@epvc.pt</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirEmail2}>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name="mail-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.texto }}>Secretaria</Text>
|
||||
</View>
|
||||
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>secretaria@epvc.pt</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.linha, { borderBottomColor: cores.borda }]} onPress={abrirTelefone}>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name="call-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.texto }}>Ligar para a Escola</Text>
|
||||
</View>
|
||||
<Text style={{ color: cores.textoSecundario, fontSize: 12 }}>252 641 805</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={[styles.linha, { borderBottomColor: cores.borda }]}>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name="information-circle-outline" size={20} color={cores.texto} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.texto }}>Versão da app</Text>
|
||||
</View>
|
||||
<Text style={{ color: cores.textoSecundario }}>26.1.10</Text>
|
||||
</View>
|
||||
{/* BANNER DE FEEDBACK */}
|
||||
{alertConfig && (
|
||||
<Animated.View style={[
|
||||
styles.alertBar,
|
||||
{
|
||||
opacity: alertOpacity,
|
||||
backgroundColor: alertConfig.type === 'error' ? cores.vermelho : alertConfig.type === 'success' ? cores.verde : cores.azul,
|
||||
top: insets.top + 10
|
||||
}
|
||||
]}>
|
||||
<Ionicons name={alertConfig.type === 'error' ? "alert-circle" : "information-circle"} size={20} color="#fff" />
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
|
||||
{/* HEADER ESTILIZADO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.linha, { borderBottomWidth: 0 }]}
|
||||
onPress={handleLogout}
|
||||
style={[styles.btnVoltar, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<View style={styles.iconTexto}>
|
||||
<Ionicons name="log-out-outline" size={22} color={cores.sair} style={{marginRight: 10}} />
|
||||
<Text style={{ color: cores.sair, fontWeight: '600' }}>Terminar Sessão</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.textoSecundario} />
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
|
||||
<View style={{ width: 45 }} />
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* GRUPO: PERSONALIZAÇÃO */}
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Personalização</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="notifications" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações Push</Text>
|
||||
<Switch
|
||||
value={notificacoes}
|
||||
onValueChange={(v) => {
|
||||
setNotificacoes(v);
|
||||
showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info");
|
||||
}}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.azul }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.item}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: isDarkMode ? '#334155' : '#F1F5F9' }]}>
|
||||
<Ionicons name={isDarkMode ? "moon" : "sunny"} size={20} color={isDarkMode ? '#FACC15' : '#F59E0B'} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Interface Escura</Text>
|
||||
<Switch
|
||||
value={isDarkMode}
|
||||
onValueChange={toggleTheme}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.azul }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* GRUPO: CONTACTOS EPVC */}
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 25 }]}>Escola Profissional</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SettingLink
|
||||
icon="business"
|
||||
label="Direção Geral"
|
||||
subLabel="epvc@epvc.pt"
|
||||
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
|
||||
cores={cores}
|
||||
showBorder
|
||||
/>
|
||||
<SettingLink
|
||||
icon="mail"
|
||||
label="Secretaria"
|
||||
subLabel="secretaria@epvc.pt"
|
||||
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
|
||||
cores={cores}
|
||||
showBorder
|
||||
/>
|
||||
<SettingLink
|
||||
icon="call"
|
||||
label="Linha Direta"
|
||||
subLabel="252 641 805"
|
||||
onPress={() => abrirURL('tel:252641805')}
|
||||
cores={cores}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* GRUPO: SEGURANÇA & INFO */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 25 }]}>
|
||||
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Versão Estável</Text>
|
||||
<Text style={[styles.versionBadge, { color: cores.secundario }]}>v2.6.17</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.item} onPress={handleLogout}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '800' }]}>Terminar Sessão</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.vermelho} opacity={0.5} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={[styles.footerText, { color: cores.secundario }]}>Desenvolvido para PAP • 2026</Text>
|
||||
<Text style={[styles.footerText, { color: cores.azul, fontWeight: '800', marginTop: 4 }]}>EPVC</Text>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
// Componente Auxiliar para Links
|
||||
const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.item, showBorder && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={20} color={cores.azul} />
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>{label}</Text>
|
||||
<Text style={{ color: cores.secundario, fontSize: 12, marginTop: 2 }}>{subLabel}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.3} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
safe: { flex: 1 },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 8 },
|
||||
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1, fontSize: 14 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
btnVoltar: { width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignItems: 'center', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2 },
|
||||
tituloGeral: { fontSize: 24, fontWeight: 'bold' },
|
||||
subtituloSecao: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase', marginBottom: 5, marginLeft: 5 },
|
||||
container: { padding: 20 },
|
||||
card: { paddingHorizontal: 20, paddingVertical: 15, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
|
||||
linha: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 15, borderBottomWidth: 1 },
|
||||
iconTexto: { flexDirection: 'row', alignItems: 'center' }
|
||||
btnVoltar: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 50 },
|
||||
sectionLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 1.2 },
|
||||
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 15, elevation: 2 },
|
||||
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16 },
|
||||
iconContainer: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700' },
|
||||
versionBadge: { fontSize: 13, fontWeight: 'bold', backgroundColor: 'rgba(0,0,0,0.05)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
footer: { alignItems: 'center', marginTop: 40, opacity: 0.6 },
|
||||
footerText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 }
|
||||
});
|
||||
|
||||
export default Definicoes;
|
||||
@@ -1,25 +1,55 @@
|
||||
// app/Aluno/PerfilAluno.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator, Alert, Platform, SafeAreaView, ScrollView, StatusBar,
|
||||
ActivityIndicator, Animated,
|
||||
ScrollView, StatusBar,
|
||||
StyleSheet, Text, TextInput, TouchableOpacity, View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
export default function PerfilAluno() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const [perfil, setPerfil] = useState<any>(null);
|
||||
const [estagio, setEstagio] = useState<any>(null);
|
||||
const [contagemPresencas, setContagemPresencas] = useState(0);
|
||||
const [contagemFaltas, setContagemFaltas] = useState(0);
|
||||
|
||||
// --- FUNÇÕES DE FORMATAÇÃO E CÁLCULO ---
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
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));
|
||||
}, []);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
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',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
laranja: '#dd8707',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const formatarParaExibir = (data: string) => {
|
||||
if (!data) return '';
|
||||
@@ -41,64 +71,33 @@ export default function PerfilAluno() {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// NOVA FUNÇÃO: Calcula horas totais excluindo fins de semana e feriados
|
||||
const calcularHorasTotaisUteis = (dataInicio: string, dataFim: string) => {
|
||||
if (!dataInicio || !dataFim) return 400; // Fallback caso a BD esteja vazia
|
||||
|
||||
if (!dataInicio || !dataFim) return 400;
|
||||
let inicio = new Date(dataInicio);
|
||||
let fim = new Date(dataFim);
|
||||
let diasUteis = 0;
|
||||
|
||||
// Lista de feriados 2026 (Exemplo Portugal)
|
||||
const feriados = [
|
||||
'2026-01-01', '2026-04-03', '2026-04-05', '2026-04-25',
|
||||
'2026-05-01', '2026-06-04', '2026-06-10', '2026-08-15',
|
||||
'2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25'
|
||||
];
|
||||
const feriados = ['2026-01-01', '2026-04-03', '2026-04-25', '2026-05-01', '2026-06-10', '2026-08-15', '2026-10-05', '2026-11-01', '2026-12-01', '2026-12-08', '2026-12-25'];
|
||||
|
||||
let dataAtual = new Date(inicio);
|
||||
while (dataAtual <= fim) {
|
||||
const diaSemana = dataAtual.getDay(); // 0: Domingo, 6: Sábado
|
||||
const diaSemana = dataAtual.getDay();
|
||||
const dataIso = dataAtual.toISOString().split('T')[0];
|
||||
|
||||
if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) {
|
||||
diasUteis++;
|
||||
}
|
||||
if (diaSemana !== 0 && diaSemana !== 6 && !feriados.includes(dataIso)) diasUteis++;
|
||||
dataAtual.setDate(dataAtual.getDate() + 1);
|
||||
}
|
||||
return diasUteis * 7; // Multiplicado pelas 7h diárias
|
||||
return diasUteis * 7;
|
||||
};
|
||||
|
||||
const calcularIdade = (dataExibicao: string) => {
|
||||
if (!dataExibicao || dataExibicao.length < 10) return '--';
|
||||
try {
|
||||
const [dia, mes, ano] = dataExibicao.split('-').map(Number);
|
||||
const hoje = new Date();
|
||||
const nascimento = new Date(ano, mes - 1, dia);
|
||||
let idade = hoje.getFullYear() - nascimento.getFullYear();
|
||||
const m = hoje.getMonth() - nascimento.getMonth();
|
||||
if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) {
|
||||
idade--;
|
||||
}
|
||||
return isNaN(idade) ? '--' : idade;
|
||||
} catch {
|
||||
return '--';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
carregarDados();
|
||||
}, []);
|
||||
|
||||
async function carregarDados() {
|
||||
const carregarDados = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const [perfilRes, estagioRes, presencasRes] = await Promise.all([
|
||||
supabase.from('profiles').select('*').eq('id', user.id).single(),
|
||||
supabase.from('estagios').select('*, empresas(*)').eq('aluno_id', user.id).single(),
|
||||
supabase.from('presencas').select('estado').eq('aluno_id', user.id)
|
||||
supabase.from('presencas').select('*').eq('aluno_id', user.id)
|
||||
]);
|
||||
|
||||
const dadosPerfil = perfilRes.data;
|
||||
@@ -107,23 +106,34 @@ export default function PerfilAluno() {
|
||||
}
|
||||
|
||||
setPerfil({ ...dadosPerfil, email: user.email });
|
||||
setEstagio(estagioRes.data);
|
||||
const dadosEstagio = estagioRes.data;
|
||||
setEstagio(dadosEstagio);
|
||||
|
||||
if (presencasRes.data) {
|
||||
setContagemPresencas(presencasRes.data.filter((p: any) => p.estado === 'presente').length);
|
||||
setContagemFaltas(presencasRes.data.filter((p: any) => p.estado === 'faltou').length);
|
||||
if (presencasRes.data && dadosEstagio) {
|
||||
const dataInicio = new Date(dadosEstagio.data_inicio);
|
||||
const dataFim = new Date(dadosEstagio.data_fim);
|
||||
|
||||
// Filtrar apenas presenças que ocorrem DENTRO do intervalo do estágio
|
||||
const presencasValidas = presencasRes.data.filter((p: any) => {
|
||||
const dataP = new Date(p.data);
|
||||
return dataP >= dataInicio && dataP <= dataFim;
|
||||
});
|
||||
|
||||
setContagemPresencas(presencasValidas.filter((p: any) => p.estado === 'presente').length);
|
||||
setContagemFaltas(presencasValidas.filter((p: any) => p.estado === 'faltou').length);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Erro ao carregar dados.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { carregarDados(); }, []);
|
||||
|
||||
const salvarPerfil = async () => {
|
||||
try {
|
||||
const dataBD = formatarParaSalvar(perfil.data_nascimento);
|
||||
|
||||
const { error } = await supabase.from('profiles').update({
|
||||
nome: perfil.nome,
|
||||
telefone: perfil.telefone,
|
||||
@@ -133,220 +143,207 @@ export default function PerfilAluno() {
|
||||
|
||||
if (error) throw error;
|
||||
setIsEditing(false);
|
||||
Alert.alert("Sucesso", "Perfil atualizado!");
|
||||
showAlert('Perfil atualizado!', 'success');
|
||||
} catch (e) {
|
||||
Alert.alert("Erro", "Verifica se a data está correta (DD-MM-AAAA).");
|
||||
showAlert('Verifica se a data está correta.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// CÁLCULOS DO DASHBOARD
|
||||
const horasPorDia = 7;
|
||||
const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim);
|
||||
const horasRealizadas = contagemPresencas * horasPorDia;
|
||||
const horasRestantes = Math.max(0, horasTotais - horasRealizadas);
|
||||
const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0;
|
||||
|
||||
const themeStyles = {
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
vermelho: '#EF4444'
|
||||
const terminarSessao = async () => {
|
||||
await supabase.auth.signOut();
|
||||
router.replace('/');
|
||||
};
|
||||
|
||||
if (loading) return <View style={[styles.centered, { backgroundColor: themeStyles.fundo }]}><ActivityIndicator size="large" color={themeStyles.azul} /></View>;
|
||||
const horasTotais = calcularHorasTotaisUteis(estagio?.data_inicio, estagio?.data_fim);
|
||||
const horasRealizadas = contagemPresencas * 7;
|
||||
const progresso = horasTotais > 0 ? Math.min(1, horasRealizadas / horasTotais) : 0;
|
||||
|
||||
if (loading) return <View style={[styles.centered, { backgroundColor: cores.fundo }]}><ActivityIndicator size="large" color={cores.azul} /></View>;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<View style={styles.topNav}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{alertConfig && (
|
||||
<Animated.View style={[styles.alertBar, { opacity: alertOpacity, backgroundColor: alertConfig.type === 'error' ? cores.vermelho : alertConfig.type === 'success' ? cores.verde : cores.azul, top: insets.top + 10 }]}>
|
||||
<Ionicons name={alertConfig.type === 'error' ? "alert-circle" : "checkmark-circle"} size={20} color="#fff" />
|
||||
<Text style={styles.alertText}>{alertConfig.msg}</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: themeStyles.azul }]}>
|
||||
<Text style={styles.avatarText}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<Text style={[styles.userName, { color: themeStyles.texto }]}>{perfil?.nome}</Text>
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity style={[styles.backBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnEdit, { backgroundColor: isEditing ? '#10B981' : themeStyles.card, borderColor: themeStyles.borda }]}
|
||||
onPress={isEditing ? salvarPerfil : () => setIsEditing(true)}
|
||||
style={[styles.editBtn, { backgroundColor: isEditing ? cores.azul : cores.card }]}
|
||||
onPress={() => isEditing ? salvarPerfil() : setIsEditing(true)}
|
||||
>
|
||||
<Ionicons name={isEditing ? "checkmark-outline" : "create-outline"} size={18} color={isEditing ? "#fff" : themeStyles.azul} />
|
||||
<Text style={[styles.btnEditText, { color: isEditing ? "#fff" : themeStyles.texto }]}>
|
||||
{isEditing ? "Guardar" : "Editar Perfil"}
|
||||
</Text>
|
||||
<Ionicons name={isEditing ? "checkmark" : "create-outline"} size={20} color={isEditing ? "#fff" : cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* PROGRESSO */}
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Progresso do Estágio</Text>
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<View style={styles.statsGrid}>
|
||||
<StatBox label="Realizadas" valor={`${horasRealizadas}h`} cor={themeStyles.azul} theme={themeStyles} />
|
||||
<StatBox label="Faltam" valor={`${horasRestantes}h`} cor="#F59E0B" theme={themeStyles} />
|
||||
<StatBox label="Faltas" valor={contagemFaltas.toString()} cor={themeStyles.vermelho} theme={themeStyles} />
|
||||
</View>
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={[styles.progressBarBase, { backgroundColor: themeStyles.fundo }]}>
|
||||
<View style={[styles.progressBarFill, { width: `${progresso * 100}%`, backgroundColor: themeStyles.azul }]} />
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={styles.profileHeader}>
|
||||
<View style={[styles.avatarContainer, { borderColor: cores.azulSuave }]}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<Text style={[styles.progressText, { color: themeStyles.secundario }]}>
|
||||
{Math.round(progresso * 100)}% das {horasTotais}h concluídas (Dias Úteis)
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
|
||||
<Text style={[styles.userRole, { color: cores.secundario }]}>Nº Aluno: {perfil?.n_escola || '---'}</Text>
|
||||
</View>
|
||||
|
||||
{/* PROGRESS CARD */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card, marginBottom: 25 }]}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario, marginBottom: 15 }]}>Progresso do Estágio</Text>
|
||||
<View style={styles.statsGrid}>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValor, { color: cores.azul }]}>{horasRealizadas}h</Text>
|
||||
<Text style={[styles.statLabel, { color: cores.secundario }]}>Feitas</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValor, { color: cores.laranja }]}>{Math.max(0, horasTotais - horasRealizadas)}h</Text>
|
||||
<Text style={[styles.statLabel, { color: cores.secundario }]}>Restam</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValor, { color: cores.vermelho }]}>{contagemFaltas}</Text>
|
||||
<Text style={[styles.statLabel, { color: cores.secundario }]}>Faltas</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.progressBarBase, { backgroundColor: cores.fundo, marginTop: 15 }]}>
|
||||
<View style={[styles.progressBarFill, { width: `${progresso * 100}%`, backgroundColor: cores.azul }]} />
|
||||
</View>
|
||||
<Text style={[styles.progressText, { color: cores.secundario }]}>{Math.round(progresso * 100)}% das {horasTotais}h úteis concluídas</Text>
|
||||
</View>
|
||||
|
||||
{/* INFO CARD */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
<ModernInput label="Nome Completo" icon="person-outline" value={perfil?.nome || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, nome: v})} cores={cores} />
|
||||
<ModernInput label="E-mail Institucional" icon="mail-outline" value={perfil?.email || ''} editable={false} cores={cores} />
|
||||
|
||||
<View style={styles.row}>
|
||||
{/* Aumentado o flex da data para 1.2 para evitar corte */}
|
||||
<View style={{ flex: 1.2, marginRight: 10 }}>
|
||||
<ModernInput
|
||||
label="Nascimento"
|
||||
icon="calendar-outline"
|
||||
value={perfil?.data_nascimento || ''}
|
||||
editable={isEditing}
|
||||
onChangeText={(v: string) => setPerfil({...perfil, data_nascimento: aplicarMascaraData(v)})}
|
||||
cores={cores}
|
||||
maxLength={10}
|
||||
keyboardType="numeric"
|
||||
placeholder="DD-MM-AAAA"
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1.3 }}>
|
||||
<ModernInput label="Telefone" icon="call-outline" value={perfil?.telefone || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, telefone: v})} keyboardType="phone-pad" cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ModernInput label="Residência" icon="location-outline" value={perfil?.residencia || ''} editable={isEditing} onChangeText={(v: string) => setPerfil({...perfil, residencia: v})} cores={cores} />
|
||||
</View>
|
||||
|
||||
{/* ESTÁGIO CARD ATUALIZADO */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card, marginTop: 20 }]}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario, marginBottom: 15 }]}>Informações do Estágio</Text>
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Início</Text>
|
||||
<Text style={{ color: cores.texto, fontWeight: '700' }}>{formatarParaExibir(estagio?.data_inicio)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Fim</Text>
|
||||
<Text style={{ color: cores.texto, fontWeight: '700' }}>{formatarParaExibir(estagio?.data_fim)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Horário</Text>
|
||||
<Text style={{ color: cores.texto, fontWeight: '700' }}>{estagio?.horario || '09:00-17:00'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ marginTop: 15 }}>
|
||||
<Text style={[styles.inputLabel, { fontSize: 9, color: cores.secundario }]}>Empresa e Tutor</Text>
|
||||
<Text style={{ color: cores.texto, fontWeight: '700' }}>{estagio?.empresas?.nome}</Text>
|
||||
<Text style={{ color: cores.secundario, fontSize: 13 }}>{estagio?.empresas?.tutor_nome}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DADOS ACADÉMICOS */}
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Dados de Aluno</Text>
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<InfoRow icon="id-card-outline" label="Nº de Aluno" valor={perfil?.n_escola || '---'} theme={themeStyles} />
|
||||
<Divider theme={themeStyles} />
|
||||
<InfoRow icon="mail-outline" label="E-mail Institucional" valor={perfil?.email} theme={themeStyles} />
|
||||
<View style={[styles.actionsContainer, { marginTop: 25 }]}>
|
||||
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card }]} onPress={() => router.push('/Aluno/redefenirsenha')}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card }]} onPress={terminarSessao}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar Sessão</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* INFORMAÇÃO PESSOAL */}
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Informação Pessoal</Text>
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<EditableRow icon="person-outline" label="Nome Completo" value={perfil?.nome} isEditing={isEditing} onChange={(t: string) => setPerfil({...perfil, nome: t})} theme={themeStyles} />
|
||||
<Divider theme={themeStyles} />
|
||||
|
||||
<EditableRow
|
||||
icon="calendar-outline"
|
||||
label="Data de Nascimento"
|
||||
value={perfil?.data_nascimento}
|
||||
isEditing={isEditing}
|
||||
onChange={(t: string) => setPerfil({...perfil, data_nascimento: aplicarMascaraData(t)})}
|
||||
theme={themeStyles}
|
||||
keyboard="numeric"
|
||||
limit={10}
|
||||
/>
|
||||
|
||||
<Divider theme={themeStyles} />
|
||||
{isEditing && (
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setIsEditing(false); carregarDados(); }}>
|
||||
<Text style={[styles.cancelText, { color: cores.secundario }]}>Cancelar Edição</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<InfoRow
|
||||
icon="hourglass-outline"
|
||||
label="Idade Atual"
|
||||
valor={calcularIdade(perfil?.data_nascimento) !== '--' ? `${calcularIdade(perfil?.data_nascimento)} anos` : '---'}
|
||||
theme={themeStyles}
|
||||
/>
|
||||
|
||||
<Divider theme={themeStyles} />
|
||||
<EditableRow icon="call-outline" label="Telemóvel" value={perfil?.telefone} isEditing={isEditing} onChange={(t: string) => setPerfil({...perfil, telefone: t})} theme={themeStyles} keyboard="phone-pad" />
|
||||
<Divider theme={themeStyles} />
|
||||
<EditableRow icon="location-outline" label="Residência" value={perfil?.residencia} isEditing={isEditing} onChange={(t: string) => setPerfil({...perfil, residencia: t})} theme={themeStyles} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* EMPRESA E DATAS DO ESTÁGIO */}
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: themeStyles.secundario }]}>Horário e Local</Text>
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}>
|
||||
<InfoRow icon="calendar-outline" label="Início do Estágio" valor={formatarParaExibir(estagio?.data_inicio)} theme={themeStyles} />
|
||||
<Divider theme={themeStyles} />
|
||||
<InfoRow icon="flag-outline" label="Fim do Estágio" valor={formatarParaExibir(estagio?.data_fim)} theme={themeStyles} />
|
||||
<Divider theme={themeStyles} />
|
||||
<InfoRow icon="time-outline" label="Horário Diário" valor={estagio?.horario || "09:00 - 17:00"} theme={themeStyles} />
|
||||
<Divider theme={themeStyles} />
|
||||
<InfoRow icon="business-outline" label="Empresa" valor={estagio?.empresas?.nome} theme={themeStyles} />
|
||||
<Divider theme={themeStyles} />
|
||||
<InfoRow icon="person-outline" label="Tutor" valor={estagio?.empresas?.tutor_nome} theme={themeStyles} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// --- AUXILIARES (IGUAIS) ---
|
||||
function StatBox({ label, valor, cor, theme }: any) {
|
||||
return (
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValor, { color: cor }]}>{valor}</Text>
|
||||
<Text style={[styles.statLabel, { color: theme.secundario }]}>{label}</Text>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableRow({ icon, label, value, isEditing, onChange, theme, keyboard = "default", limit }: any) {
|
||||
return (
|
||||
<View style={styles.infoRow}>
|
||||
<Ionicons name={icon} size={20} color={theme.azul} style={{ width: 30 }} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.infoLabel, { color: theme.secundario }]}>{label}</Text>
|
||||
{isEditing ? (
|
||||
<TextInput
|
||||
style={[styles.input, { color: theme.texto, borderBottomColor: theme.azul }]}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
keyboardType={keyboard}
|
||||
maxLength={limit}
|
||||
placeholder={label.includes("Data") ? "DD-MM-AAAA" : ""}
|
||||
placeholderTextColor={theme.secundario}
|
||||
/>
|
||||
) : (
|
||||
<Text style={[styles.infoValor, { color: theme.texto }]}>{value || '---'}</Text>
|
||||
)}
|
||||
</View>
|
||||
const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<View style={[styles.inputContainer, { backgroundColor: cores.fundo, borderColor: editable ? cores.azul : cores.borda, opacity: editable ? 1 : 0.8 }]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 8 }} />
|
||||
<TextInput {...props} editable={editable} style={[styles.textInput, { color: cores.texto }]} placeholderTextColor={cores.secundario} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ icon, label, valor, theme }: any) {
|
||||
return (
|
||||
<View style={styles.infoRow}>
|
||||
<Ionicons name={icon} size={20} color={theme.azul} style={{ width: 30 }} />
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={[styles.infoLabel, { color: theme.secundario }]}>{label}</Text>
|
||||
<Text style={[styles.infoValor, { color: theme.texto }]}>{valor || '---'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Divider = ({ theme }: any) => <View style={[styles.divider, { backgroundColor: theme.borda }]} />;
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
topNav: { paddingHorizontal: 20, paddingTop: 10, height: 50, justifyContent: 'center' },
|
||||
backBtn: { width: 40, height: 40, justifyContent: 'center' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
safe: { flex: 1 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
header: { alignItems: 'center', marginBottom: 30 },
|
||||
avatarCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
||||
avatarText: { fontSize: 32, fontWeight: 'bold', color: '#fff' },
|
||||
userName: { fontSize: 20, fontWeight: '800', marginBottom: 15 },
|
||||
btnEdit: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, borderWidth: 1 },
|
||||
btnEditText: { marginLeft: 8, fontSize: 13, fontWeight: '600' },
|
||||
section: { marginBottom: 25 },
|
||||
sectionTitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10, marginLeft: 5 },
|
||||
card: { borderRadius: 20, padding: 16, borderWidth: 1 },
|
||||
statsGrid: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 20 },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 15, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 10 },
|
||||
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 },
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 15 },
|
||||
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 },
|
||||
topTitle: { fontSize: 18, fontWeight: '800' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
profileHeader: { alignItems: 'center', marginVertical: 20 },
|
||||
avatarContainer: { padding: 6, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' },
|
||||
avatar: { width: 70, height: 70, borderRadius: 35, alignItems: 'center', justifyContent: 'center', elevation: 4 },
|
||||
avatarLetter: { color: '#fff', fontSize: 28, fontWeight: '800' },
|
||||
userName: { fontSize: 20, fontWeight: '800', marginTop: 12 },
|
||||
userRole: { fontSize: 13, fontWeight: '500' },
|
||||
card: { borderRadius: 24, padding: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 },
|
||||
statsGrid: { flexDirection: 'row', justifyContent: 'space-around' },
|
||||
statBox: { alignItems: 'center' },
|
||||
statValor: { fontSize: 18, fontWeight: 'bold' },
|
||||
statLabel: { fontSize: 11, fontWeight: '600', marginTop: 2 },
|
||||
progressContainer: { marginTop: 10 },
|
||||
statValor: { fontSize: 18, fontWeight: '800' },
|
||||
statLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
|
||||
progressBarBase: { height: 8, borderRadius: 4, overflow: 'hidden' },
|
||||
progressBarFill: { height: '100%', borderRadius: 4 },
|
||||
progressText: { fontSize: 11, textAlign: 'center', marginTop: 8, fontWeight: '600' },
|
||||
infoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8 },
|
||||
infoContent: { flex: 1 },
|
||||
infoLabel: { fontSize: 10, fontWeight: '600', textTransform: 'uppercase' },
|
||||
infoValor: { fontSize: 15, fontWeight: '600', marginTop: 2 },
|
||||
input: { fontSize: 15, fontWeight: '600', paddingVertical: 2, borderBottomWidth: 1 },
|
||||
divider: { height: 1, width: '100%', marginVertical: 8 }
|
||||
progressText: { fontSize: 10, textAlign: 'center', marginTop: 8, fontWeight: '700' },
|
||||
inputWrapper: { marginBottom: 15 },
|
||||
inputLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, height: 50, borderRadius: 16, borderWidth: 1.5 },
|
||||
textInput: { flex: 1, fontSize: 14, fontWeight: '600' },
|
||||
row: { flexDirection: 'row' },
|
||||
actionsContainer: { gap: 10 },
|
||||
menuItem: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 18, elevation: 1 },
|
||||
menuIcon: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
menuText: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' },
|
||||
cancelBtn: { marginTop: 20, alignItems: 'center' },
|
||||
cancelText: { fontSize: 13, fontWeight: '600', textDecorationLine: 'underline' }
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// app/(Professor)/HistoricoPresencas.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -40,15 +41,16 @@ export default function HistoricoPresencas() {
|
||||
const nomeStr = Array.isArray(params.nome) ? params.nome[0] : params.nome;
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#FFFFFF' : '#1A1C1E',
|
||||
secundario: isDarkMode ? '#8E8E93' : '#6C757D',
|
||||
azul: '#2390a6',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
vermelho: '#FF453A',
|
||||
verde: '#32D74B',
|
||||
borda: isDarkMode ? '#2C2C2E' : '#E9ECEF',
|
||||
linha: isDarkMode ? '#2C2C2E' : '#D1D9E6',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -58,16 +60,41 @@ export default function HistoricoPresencas() {
|
||||
async function fetchHistorico() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const hoje = new Date().toISOString().split('T')[0];
|
||||
|
||||
// 1. Procurar o estágio ativo (mesma lógica das Faltas)
|
||||
const { data: listaEstagios, error: errEstagio } = await supabase
|
||||
.from('estagios')
|
||||
.select('data_inicio, data_fim')
|
||||
.lte('data_inicio', hoje)
|
||||
.gte('data_fim', hoje)
|
||||
.limit(1);
|
||||
|
||||
if (errEstagio) throw errEstagio;
|
||||
|
||||
// Fallback caso não haja estágio hoje (Janeiro a Dezembro de 2026)
|
||||
let inicio = '2026-01-01';
|
||||
let fim = '2026-12-31';
|
||||
|
||||
if (listaEstagios && listaEstagios.length > 0) {
|
||||
inicio = listaEstagios[0].data_inicio;
|
||||
fim = listaEstagios[0].data_fim;
|
||||
}
|
||||
|
||||
// 2. Buscar histórico filtrado por esse período
|
||||
const { data, error } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.eq('aluno_id', idStr)
|
||||
.gte('data', inicio)
|
||||
.lte('data', fim)
|
||||
.order('data', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setPresencas(data || []);
|
||||
} catch (error: any) {
|
||||
Alert.alert("Erro", "Não foi possível carregar as presenças.");
|
||||
console.error(error);
|
||||
Alert.alert("Erro", "Não foi possível carregar as presenças do estágio atual.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -97,9 +124,7 @@ export default function HistoricoPresencas() {
|
||||
});
|
||||
|
||||
if (url) {
|
||||
Linking.openURL(url).catch(() => {
|
||||
Alert.alert("Erro", "Não foi possível abrir o mapa.");
|
||||
});
|
||||
Linking.openURL(url).catch(() => Alert.alert("Erro", "Não foi possível abrir o mapa."));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,26 +132,25 @@ export default function HistoricoPresencas() {
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* HEADER PREMIUM */}
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
<View style={styles.headerArea}>
|
||||
<View style={styles.topRow}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color={cores.texto} />
|
||||
<Ionicons name="arrow-back" size={20} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Histórico</Text>
|
||||
<Text style={[styles.subtitle, { color: cores.secundario }]} numberOfLines={1}>{nomeStr}</Text>
|
||||
<View style={styles.titleWrapper}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Histórico</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.azul }]} numberOfLines={1}>{nomeStr}</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={fetchHistorico}
|
||||
style={[styles.refreshBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
>
|
||||
<Ionicons name="refresh" size={20} color={cores.azul} />
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -138,63 +162,65 @@ export default function HistoricoPresencas() {
|
||||
) : (
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
{presencas.length === 0 ? (
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="calendar-outline" size={48} color={cores.secundario} style={{ opacity: 0.5 }} />
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Sem registos para este aluno.</Text>
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={[styles.emptyIconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="calendar-clear-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem registos neste estágio</Text>
|
||||
<Text style={[styles.emptySub, { color: cores.secundario }]}>Não foram encontradas presenças ou faltas para o período selecionado.</Text>
|
||||
</View>
|
||||
) : (
|
||||
presencas.map((item) => {
|
||||
const isPresente = item.estado === 'presente';
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => handleNavigation(item)}
|
||||
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[styles.iconBox, { backgroundColor: isPresente ? cores.azulSuave : 'rgba(239, 68, 68, 0.1)' }]}>
|
||||
<Ionicons
|
||||
name={isPresente ? "book-outline" : "alert-circle-outline"}
|
||||
size={20}
|
||||
color={isPresente ? cores.azul : cores.vermelho}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.dateText, { color: cores.texto }]}>
|
||||
{new Date(item.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</Text>
|
||||
<View style={styles.statusBadge}>
|
||||
<View style={[styles.dot, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>
|
||||
{item.estado ? item.estado.toUpperCase() : '---'}
|
||||
</Text>
|
||||
<View style={styles.timelineWrapper}>
|
||||
<View style={[styles.timelineLine, { backgroundColor: cores.linha }]} />
|
||||
|
||||
{presencas.map((item) => {
|
||||
const isPresente = item.estado === 'presente';
|
||||
const dataObj = new Date(item.data);
|
||||
|
||||
return (
|
||||
<View key={item.id} style={styles.timelineItem}>
|
||||
<View style={[styles.timelineDot, { backgroundColor: isPresente ? cores.verde : cores.vermelho, borderColor: cores.fundo }]} />
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
onPress={() => handleNavigation(item)}
|
||||
style={[styles.modernCard, { backgroundColor: cores.card }]}
|
||||
>
|
||||
<View style={styles.cardMain}>
|
||||
<View style={styles.dateInfo}>
|
||||
<Text style={[styles.dayText, { color: cores.texto }]}>
|
||||
{dataObj.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
|
||||
</Text>
|
||||
<Text style={[styles.yearText, { color: cores.secundario }]}>
|
||||
{dataObj.getFullYear()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.dividerVertical} />
|
||||
|
||||
<View style={styles.contentInfo}>
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={[styles.statusText, { color: isPresente ? cores.azul : cores.vermelho }]}>
|
||||
{isPresente ? 'PRESENÇA MARCADA' : 'FALTA REGISTADA'}
|
||||
</Text>
|
||||
{item.lat && item.lng && (
|
||||
<TouchableOpacity onPress={() => abrirMapa(item.lat!, item.lng!)} style={styles.mapSmallBtn}>
|
||||
<Ionicons name="location" size={14} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.infoHint, { color: cores.secundario }]}>
|
||||
Clique para ver {isPresente ? 'o sumário' : 'a justificação'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={16} color={cores.borda} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
{item.lat && item.lng && (
|
||||
<TouchableOpacity
|
||||
onPress={() => abrirMapa(item.lat!, item.lng!)}
|
||||
style={styles.locationBtn}
|
||||
>
|
||||
<Ionicons name="location-sharp" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.footerCard, { borderTopColor: isDarkMode ? '#2D2D2D' : '#F1F5F9' }]}>
|
||||
<Text style={[styles.tapInfo, { color: isPresente ? cores.azul : cores.vermelho }]}>
|
||||
Ver {isPresente ? 'sumário detalhado' : 'comprovativo de falta'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
@@ -204,40 +230,31 @@ export default function HistoricoPresencas() {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 },
|
||||
headerFixed: { paddingHorizontal: 20, paddingBottom: 10 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 75 },
|
||||
backBtnPremium: {
|
||||
width: 42, height: 42, borderRadius: 14,
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
borderWidth: 1, elevation: 2, shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2
|
||||
},
|
||||
refreshBtnPremium: {
|
||||
width: 42, height: 42, borderRadius: 14,
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
borderWidth: 1
|
||||
},
|
||||
titleContainer: { flex: 1, alignItems: 'center', paddingHorizontal: 10 },
|
||||
title: { fontSize: 20, fontWeight: '800' },
|
||||
subtitle: { fontSize: 12, fontWeight: '600', marginTop: -2 },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
|
||||
card: {
|
||||
borderRadius: 22, padding: 16, marginBottom: 15,
|
||||
borderWidth: 1, elevation: 3, shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10
|
||||
},
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center' },
|
||||
iconBox: { width: 44, height: 44, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
dateText: { fontSize: 15, fontWeight: '700' },
|
||||
statusBadge: { flexDirection: 'row', alignItems: 'center', marginTop: 4 },
|
||||
dot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 },
|
||||
subText: { fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
|
||||
actionRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
locationBtn: { width: 34, height: 34, justifyContent: 'center', alignItems: 'center' },
|
||||
footerCard: { marginTop: 12, paddingTop: 10, borderTopWidth: 1 },
|
||||
tapInfo: { fontSize: 10, fontWeight: '800', textAlign: 'right', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
headerArea: { paddingHorizontal: 24, paddingBottom: 15, paddingTop: 10 },
|
||||
topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 60 },
|
||||
btnAction: { width: 45, height: 45, borderRadius: 16, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
titleWrapper: { alignItems: 'center', flex: 1 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 },
|
||||
scrollContent: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 50 },
|
||||
timelineWrapper: { paddingLeft: 10 },
|
||||
timelineLine: { position: 'absolute', left: 10, top: 0, bottom: 0, width: 2, borderRadius: 1 },
|
||||
timelineItem: { marginBottom: 20, paddingLeft: 25, justifyContent: 'center' },
|
||||
timelineDot: { position: 'absolute', left: 6, top: '50%', marginTop: -5, width: 10, height: 10, borderRadius: 5, borderWidth: 2, zIndex: 1 },
|
||||
modernCard: { borderRadius: 20, padding: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8 },
|
||||
cardMain: { flexDirection: 'row', alignItems: 'center' },
|
||||
dateInfo: { alignItems: 'center', minWidth: 50 },
|
||||
dayText: { fontSize: 15, fontWeight: '800', textTransform: 'uppercase' },
|
||||
yearText: { fontSize: 11, fontWeight: '600' },
|
||||
dividerVertical: { width: 1, height: 30, backgroundColor: 'rgba(128,128,128,0.2)', marginHorizontal: 15 },
|
||||
contentInfo: { flex: 1 },
|
||||
statusRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 2 },
|
||||
statusText: { fontSize: 11, fontWeight: '900', letterSpacing: 0.5 },
|
||||
mapSmallBtn: { marginLeft: 8 },
|
||||
infoHint: { fontSize: 12, fontWeight: '500' },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
empty: { alignItems: 'center', marginTop: 60 },
|
||||
emptyText: { marginTop: 15, fontSize: 14, fontWeight: '600', textAlign: 'center' },
|
||||
emptyContainer: { alignItems: 'center', marginTop: 100 },
|
||||
emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
emptyText: { fontSize: 18, fontWeight: '800', marginBottom: 8 },
|
||||
emptySub: { fontSize: 14, textAlign: 'center', paddingHorizontal: 40, lineHeight: 20 },
|
||||
});
|
||||
@@ -4,55 +4,41 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
interface AlunoEstado {
|
||||
id: string;
|
||||
nome: string;
|
||||
n_escola: string;
|
||||
turma_curso: string;
|
||||
email: string;
|
||||
telefone: string;
|
||||
residencia: string;
|
||||
idade: string;
|
||||
empresa_nome: string;
|
||||
tutor_nome: string;
|
||||
tutor_tel: string;
|
||||
data_inicio: string;
|
||||
data_fim: string;
|
||||
horas_diarias: string;
|
||||
horarios_detalhados: string[];
|
||||
}
|
||||
|
||||
const DetalhesAlunos = memo(() => {
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams();
|
||||
const { isDarkMode } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const alunoId = typeof params.alunoId === 'string' ? params.alunoId : null;
|
||||
const [aluno, setAluno] = useState<AlunoEstado | null>(null);
|
||||
const [aluno, setAluno] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchAluno = async () => {
|
||||
@@ -72,190 +58,196 @@ const DetalhesAlunos = memo(() => {
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (data) {
|
||||
const d = data as any;
|
||||
|
||||
let listaHorarios: string[] = [];
|
||||
if (data?.estagios?.[0]?.id) {
|
||||
const { data: hData } = await supabase
|
||||
.from('horarios_estagio')
|
||||
.select('hora_inicio, hora_fim')
|
||||
.eq('estagio_id', data.estagios[0].id);
|
||||
|
||||
// Garante que pegamos o objeto correto mesmo que venha em Array
|
||||
const perfil = Array.isArray(d.profiles) ? d.profiles[0] : d.profiles;
|
||||
const estagio = Array.isArray(d.estagios) ? d.estagios[0] : d.estagios;
|
||||
const empresa = estagio && (Array.isArray(estagio.empresas) ? estagio.empresas[0] : estagio.empresas);
|
||||
|
||||
let listaHorarios: string[] = [];
|
||||
if (estagio?.id) {
|
||||
const { data: hData, error: hError } = await supabase
|
||||
.from('horarios_estagio')
|
||||
.select('hora_inicio, hora_fim')
|
||||
.eq('estagio_id', estagio.id);
|
||||
|
||||
if (!hError && hData) {
|
||||
listaHorarios = hData.map(h => {
|
||||
// Limpa os segundos caso venham da base de dados (ex: 08:00:00 -> 08:00)
|
||||
const inicio = h.hora_inicio?.slice(0, 5) || '00:00';
|
||||
const fim = h.hora_fim?.slice(0, 5) || '00:00';
|
||||
return `${inicio} às ${fim}`;
|
||||
});
|
||||
}
|
||||
if (hData) {
|
||||
listaHorarios = hData.map(h => `${h.hora_inicio?.slice(0, 5)} - ${h.hora_fim?.slice(0, 5)}`);
|
||||
}
|
||||
|
||||
setAluno({
|
||||
id: String(d.id),
|
||||
nome: d.nome || 'Sem nome',
|
||||
n_escola: String(d.n_escola || '-'),
|
||||
turma_curso: d.turma_curso || '-',
|
||||
email: perfil?.email ?? '-',
|
||||
telefone: perfil?.telefone ?? '-',
|
||||
residencia: perfil?.residencia ?? '-',
|
||||
idade: perfil?.idade ? String(perfil.idade) : '-',
|
||||
empresa_nome: empresa?.nome || 'Não atribuída',
|
||||
tutor_nome: empresa?.tutor_nome || 'Não definido',
|
||||
tutor_tel: empresa?.tutor_telefone || '-',
|
||||
data_inicio: estagio?.data_inicio || '-',
|
||||
data_fim: estagio?.data_fim || '-',
|
||||
horas_diarias: estagio?.horas_diarias ? `${estagio.horas_diarias}h` : '0h',
|
||||
horarios_detalhados: listaHorarios,
|
||||
});
|
||||
}
|
||||
|
||||
setAluno({
|
||||
...data,
|
||||
perfil: Array.isArray(data.profiles) ? data.profiles[0] : data.profiles,
|
||||
estagio: Array.isArray(data.estagios) ? data.estagios[0] : data.estagios,
|
||||
horarios: listaHorarios
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.log('Erro ao carregar aluno:', err.message);
|
||||
console.log('Erro:', err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (alunoId) fetchAluno();
|
||||
}, [alunoId]);
|
||||
useEffect(() => { if (alunoId) fetchAluno(); }, [alunoId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (loading) return (
|
||||
<View style={[styles.centered, { backgroundColor: cores.fundo }]}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
|
||||
{/* HEADER */}
|
||||
{/* HEADER CLEAN */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.btnAction, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha do Aluno</Text>
|
||||
<TouchableOpacity onPress={fetchAluno} style={[styles.btnAction, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="refresh-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha Aluno</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
|
||||
Detalhes acadêmicos e estágio
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* SECÇÃO PERFIL */}
|
||||
<View style={[styles.profileCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarTxt, { color: cores.azul }]}>{aluno?.nome.charAt(0).toUpperCase()}</Text>
|
||||
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 30 }]} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* PERFIL MINIMALISTA */}
|
||||
<View style={styles.profileSection}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarTxt, { color: cores.azul }]}>{aluno?.nome?.charAt(0)}</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno?.nome}</Text>
|
||||
<Text style={[styles.alunoCurso, { color: cores.secundario }]}>{aluno?.turma_curso}</Text>
|
||||
<Text style={[styles.alunoCurso, { color: cores.laranja }]}>{aluno?.turma_curso}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DADOS PESSOAIS */}
|
||||
{/* DADOS PESSOAIS CARD */}
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<DetailRow icon="school-outline" label="Nº Escola" value={aluno?.n_escola} cores={cores} />
|
||||
<DetailRow icon="mail-outline" label="EmailInstitucional" value={aluno?.perfil?.email} cores={cores}
|
||||
onPress={() => Linking.openURL(`mailto:${aluno?.perfil?.email}`)} />
|
||||
<DetailRow icon="call-outline" label="Telemóvel" value={aluno?.perfil?.telefone} cores={cores}
|
||||
onPress={() => Linking.openURL(`tel:${aluno?.perfil?.telefone}`)} />
|
||||
<DetailRow icon="location-outline" label="Residência" value={aluno?.perfil?.residencia} cores={cores} ultimo />
|
||||
</View>
|
||||
|
||||
{/* SECÇÃO ESTÁGIO */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Pessoais</Text>
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Plano de Estágio</Text>
|
||||
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
|
||||
</View>
|
||||
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<DetailRow icon="school" label="Nº Escola" value={aluno?.n_escola} cores={cores} />
|
||||
<DetailRow icon="mail" label="Email" value={aluno?.email} cores={cores} />
|
||||
<DetailRow icon="call" label="Contacto" value={aluno?.telefone} cores={cores} />
|
||||
<DetailRow icon="location" label="Morada" value={aluno?.residencia} cores={cores} ultimo />
|
||||
</View>
|
||||
{aluno?.estagio ? (
|
||||
<View style={[styles.estagioCard, { backgroundColor: cores.azul }]}>
|
||||
<View style={styles.estagioHeader}>
|
||||
<Ionicons name="business" size={28} color="rgba(255,255,255,0.4)" />
|
||||
<View style={styles.statusBadge}><Text style={styles.statusText}>ATIVO</Text></View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.empresaNome}>{aluno?.estagio?.empresas?.nome || 'Empresa Indefinida'}</Text>
|
||||
|
||||
<View style={styles.tutorInfo}>
|
||||
<Text style={styles.miniLabel}>TUTOR NA EMPRESA</Text>
|
||||
<Text style={styles.tutorNome}>{aluno?.estagio?.empresas?.tutor_nome || 'Não definido'}</Text>
|
||||
<Text style={styles.tutorTel}>{aluno?.estagio?.empresas?.tutor_telefone || '-'}</Text>
|
||||
</View>
|
||||
|
||||
{/* INFORMAÇÃO DE ESTÁGIO */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Estágio Atual</Text>
|
||||
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
|
||||
</View>
|
||||
<View style={styles.horarioBox}>
|
||||
<Text style={styles.miniLabel}>HORÁRIOS</Text>
|
||||
{aluno?.horarios && aluno.horarios.length > 0 ? (
|
||||
aluno.horarios.map((h: string, i: number) => (
|
||||
<Text key={i} style={styles.horarioTxt}>{h}</Text>
|
||||
))
|
||||
) : (
|
||||
<Text style={styles.horarioTxt}>Não registado</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={[styles.infoCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<DetailRow icon="business" label="Empresa" value={aluno?.empresa_nome} cores={cores} />
|
||||
<DetailRow icon="person-circle" label="Tutor" value={aluno?.tutor_nome} cores={cores} />
|
||||
|
||||
<View style={styles.row}>
|
||||
<Ionicons name="time" size={18} color={cores.azul} style={styles.rowIcon} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.rowLabel, { color: cores.secundario }]}>Horários Registados</Text>
|
||||
{aluno?.horarios_detalhados && aluno.horarios_detalhados.length > 0 ? (
|
||||
aluno.horarios_detalhados.map((h, i) => (
|
||||
<Text key={i} style={[styles.rowValue, { color: cores.texto }]}>{h}</Text>
|
||||
))
|
||||
) : (
|
||||
<Text style={[styles.rowValue, { color: cores.texto }]}>Não definidos</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.estagioDivider} />
|
||||
|
||||
{/* PERÍODO CORRIGIDO: DUAS COLUNAS COM ÍCONE */}
|
||||
<View style={styles.estagioFooter}>
|
||||
<View style={styles.periodoCol}>
|
||||
<Ionicons name="calendar-outline" size={16} color="rgba(255,255,255,0.6)" style={{marginRight: 8}} />
|
||||
<View>
|
||||
<Text style={styles.miniLabel}>INÍCIO</Text>
|
||||
<Text style={styles.footerVal}>{aluno?.estagio?.data_inicio}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.periodoCol, {alignItems: 'flex-end'}]}>
|
||||
<View style={{marginRight: 8, alignItems: 'flex-end'}}>
|
||||
<Text style={styles.miniLabel}>FIM PREVISTO</Text>
|
||||
<Text style={styles.footerVal}>{aluno?.estagio?.data_fim}</Text>
|
||||
</View>
|
||||
<Ionicons name="calendar" size={16} color="rgba(255,255,255,0.6)" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.datesRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<DetailRow icon="calendar" label="Início" value={aluno?.data_inicio} cores={cores} ultimo />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<DetailRow icon="calendar-outline" label="Fim" value={aluno?.data_fim} cores={cores} ultimo />
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.noEstagio, { borderColor: cores.borda, backgroundColor: cores.card }]}>
|
||||
<Ionicons name="alert-circle-outline" size={24} color={cores.laranja} />
|
||||
<Text style={[styles.noEstagioTxt, { color: cores.secundario }]}>Sem estágio atribuído</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const DetailRow = ({ icon, label, value, cores, ultimo }: any) => (
|
||||
<View style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={styles.rowIcon} />
|
||||
const DetailRow = ({ icon, label, value, cores, ultimo, onPress }: any) => (
|
||||
<TouchableOpacity
|
||||
activeOpacity={onPress ? 0.6 : 1}
|
||||
onPress={onPress}
|
||||
style={[styles.row, !ultimo && { borderBottomWidth: 1, borderBottomColor: cores.borda + '50' }]}
|
||||
>
|
||||
<Ionicons name={icon} size={20} color={cores.azul} style={{ marginRight: 15 }} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.rowLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<Text style={[styles.rowValue, { color: cores.texto }]}>{value}</Text>
|
||||
<Text style={[styles.rowValue, { color: cores.texto }]}>{value || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{onPress && <Ionicons name="chevron-forward" size={14} color={cores.borda} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 13, fontWeight: '600' },
|
||||
listPadding: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 15 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 },
|
||||
sectionLine: { flex: 1, height: 1, borderRadius: 1 },
|
||||
profileCard: { flexDirection: 'row', alignItems: 'center', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 10 },
|
||||
avatarCircle: { width: 60, height: 60, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
|
||||
avatarTxt: { fontSize: 24, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 18, fontWeight: '800' },
|
||||
alunoCurso: { fontSize: 14, fontWeight: '600', marginTop: 2 },
|
||||
infoCard: { borderRadius: 24, borderWidth: 1, paddingHorizontal: 15, paddingVertical: 5 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900' },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
profileSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 25, gap: 18 },
|
||||
avatar: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarTxt: { fontSize: 26, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
alunoCurso: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase' },
|
||||
infoCard: { borderRadius: 25, borderWidth: 1, paddingHorizontal: 20, paddingVertical: 5, marginBottom: 30 },
|
||||
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 15 },
|
||||
rowIcon: { marginRight: 15, width: 20 },
|
||||
rowLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 2 },
|
||||
rowValue: { fontSize: 15, fontWeight: '700' },
|
||||
datesRow: { flexDirection: 'row' }
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginRight: 15 },
|
||||
sectionLine: { flex: 1, height: 1, opacity: 0.3 },
|
||||
estagioCard: { padding: 25, borderRadius: 32, elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 },
|
||||
estagioHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
|
||||
statusBadge: { backgroundColor: 'rgba(255,255,255,0.2)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
statusText: { color: '#fff', fontSize: 10, fontWeight: '900' },
|
||||
empresaNome: { color: '#fff', fontSize: 24, fontWeight: '900', marginBottom: 20 },
|
||||
tutorInfo: { marginBottom: 15 },
|
||||
miniLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, fontWeight: '900', marginBottom: 4 },
|
||||
tutorNome: { color: '#fff', fontSize: 16, fontWeight: '800' },
|
||||
tutorTel: { color: 'rgba(255,255,255,0.8)', fontSize: 13, fontWeight: '600' },
|
||||
horarioBox: { marginBottom: 20, backgroundColor: 'rgba(255,255,255,0.1)', padding: 12, borderRadius: 15 },
|
||||
horarioTxt: { color: '#fff', fontSize: 14, fontWeight: '700' },
|
||||
estagioDivider: { height: 1, backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 20 },
|
||||
|
||||
// ESTILOS DO RODAPÉ CORRIGIDOS
|
||||
estagioFooter: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
periodoCol: { flex: 1, flexDirection: 'row', alignItems: 'center' },
|
||||
footerVal: { color: '#fff', fontSize: 14, fontWeight: '800' },
|
||||
|
||||
noEstagio: { padding: 30, borderRadius: 25, borderWidth: 1, borderStyle: 'dashed', alignItems: 'center', gap: 10 },
|
||||
noEstagioTxt: { fontWeight: '700', fontSize: 14 }
|
||||
});
|
||||
|
||||
export default DetalhesAlunos;
|
||||
@@ -1,7 +1,9 @@
|
||||
// app/Professor/Estagios/index.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Modal,
|
||||
ScrollView,
|
||||
@@ -34,32 +36,31 @@ export default function Estagios() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const azulPetroleo = '#2390a6';
|
||||
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
vermelho: '#EF4444',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.5)',
|
||||
overlay: 'rgba(26, 54, 93, 0.8)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Estados de Dados
|
||||
// Estados
|
||||
const [estagios, setEstagios] = useState<Estagio[]>([]);
|
||||
const [alunos, setAlunos] = useState<Aluno[]>([]);
|
||||
const [empresas, setEmpresas] = useState<Empresa[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Estados de Modais
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [estagioParaApagar, setEstagioParaApagar] = useState<{id: string, nome: string} | null>(null);
|
||||
|
||||
// Estados de Formulário
|
||||
const [passo, setPasso] = useState(1);
|
||||
const [alunoSelecionado, setAlunoSelecionado] = useState<Aluno | null>(null);
|
||||
const [empresaSelecionada, setEmpresaSelecionada] = useState<Empresa | null>(null);
|
||||
@@ -68,13 +69,11 @@ export default function Estagios() {
|
||||
const [dataFim, setDataFim] = useState('');
|
||||
const [searchMain, setSearchMain] = useState('');
|
||||
|
||||
// Horários
|
||||
const [hManhaIni, setHManhaIni] = useState('');
|
||||
const [hManhaFim, setHManhaFim] = useState('');
|
||||
const [hTardeIni, setHTardeIni] = useState('');
|
||||
const [hTardeFim, setHTardeFim] = useState('');
|
||||
|
||||
// --- Lógica de Dados ---
|
||||
useEffect(() => { fetchDados(); }, []);
|
||||
|
||||
const fetchDados = async () => {
|
||||
@@ -91,9 +90,25 @@ export default function Estagios() {
|
||||
} catch (e) { console.error(e); } finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const carregarHorariosEdicao = async (estagioId: string) => {
|
||||
const { data } = await supabase.from('horarios_estagio').select('*').eq('estagio_id', estagioId);
|
||||
if (data) {
|
||||
setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
|
||||
data.forEach(h => {
|
||||
if (h.periodo === 'Manhã') {
|
||||
setHManhaIni(h.hora_inicio?.slice(0, 5) || '');
|
||||
setHManhaFim(h.hora_fim?.slice(0, 5) || '');
|
||||
} else if (h.periodo === 'Tarde') {
|
||||
setHTardeIni(h.hora_inicio?.slice(0, 5) || '');
|
||||
setHTardeFim(h.hora_fim?.slice(0, 5) || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const totalHorasDiarias = useMemo(() => {
|
||||
const calcularMinutos = (ini: string, fim: string) => {
|
||||
if (!ini || !fim) return 0;
|
||||
if (!ini || !fim || !ini.includes(':') || !fim.includes(':')) return 0;
|
||||
const [hI, mI] = ini.split(':').map(Number);
|
||||
const [hF, mF] = fim.split(':').map(Number);
|
||||
const totalI = (hI || 0) * 60 + (mI || 0);
|
||||
@@ -106,197 +121,207 @@ export default function Estagios() {
|
||||
return m > 0 ? `${h}h${m}m` : `${h}h`;
|
||||
}, [hManhaIni, hManhaFim, hTardeIni, hTardeFim]);
|
||||
|
||||
// --- Funções de Ação ---
|
||||
const prepararApagar = (id: string, nome: string) => {
|
||||
setEstagioParaApagar({ id, nome });
|
||||
setDeleteModalVisible(true);
|
||||
const formatarHoraPostgres = (time: string) => {
|
||||
if (!time || !time.includes(':')) return null;
|
||||
const [h, m] = time.split(':');
|
||||
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const salvarEstagio = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (empresaSelecionada) {
|
||||
await supabase.from('empresas').update({
|
||||
tutor_nome: empresaSelecionada.tutor_nome,
|
||||
tutor_telefone: empresaSelecionada.tutor_telefone
|
||||
}).eq('id', empresaSelecionada.id);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
aluno_id: alunoSelecionado?.id,
|
||||
empresa_id: empresaSelecionada?.id,
|
||||
professor_id: user?.id,
|
||||
data_inicio: dataInicio || new Date().toISOString().split('T')[0],
|
||||
data_fim: dataFim || null,
|
||||
horas_diarias: totalHorasDiarias,
|
||||
estado: 'Ativo',
|
||||
};
|
||||
|
||||
const { data: estData, error: errE } = editandoEstagio
|
||||
? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single()
|
||||
: await supabase.from('estagios').insert([payload]).select().single();
|
||||
|
||||
if (errE) throw errE;
|
||||
const currentId = editandoEstagio?.id || estData.id;
|
||||
|
||||
await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId);
|
||||
const novosHorarios = [];
|
||||
if (hManhaIni && hManhaFim) {
|
||||
novosHorarios.push({ estagio_id: currentId, periodo: 'Manhã', hora_inicio: formatarHoraPostgres(hManhaIni), hora_fim: formatarHoraPostgres(hManhaFim) });
|
||||
}
|
||||
if (hTardeIni && hTardeFim) {
|
||||
novosHorarios.push({ estagio_id: currentId, periodo: 'Tarde', hora_inicio: formatarHoraPostgres(hTardeIni), hora_fim: formatarHoraPostgres(hTardeFim) });
|
||||
}
|
||||
if (novosHorarios.length > 0) await supabase.from('horarios_estagio').insert(novosHorarios);
|
||||
|
||||
handleFecharModal();
|
||||
fetchDados();
|
||||
} catch (error: any) { Alert.alert("Erro", error.message); } finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const confirmarEliminacao = async () => {
|
||||
if (!estagioParaApagar) return;
|
||||
setLoading(true);
|
||||
const { error } = await supabase.from('estagios').delete().eq('id', estagioParaApagar.id);
|
||||
if (error) Alert.alert("Erro", "Não foi possível apagar.");
|
||||
else {
|
||||
setDeleteModalVisible(false);
|
||||
setEstagioParaApagar(null);
|
||||
fetchDados();
|
||||
}
|
||||
if (!error) { setDeleteModalVisible(false); fetchDados(); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const salvarEstagio = async () => {
|
||||
setLoading(true);
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (empresaSelecionada) {
|
||||
await supabase.from('empresas').update({
|
||||
tutor_nome: empresaSelecionada.tutor_nome,
|
||||
tutor_telefone: empresaSelecionada.tutor_telefone
|
||||
}).eq('id', empresaSelecionada.id);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
aluno_id: alunoSelecionado?.id,
|
||||
empresa_id: empresaSelecionada?.id,
|
||||
professor_id: user?.id,
|
||||
data_inicio: dataInicio || new Date().toISOString().split('T')[0],
|
||||
data_fim: dataFim || null,
|
||||
horas_diarias: totalHorasDiarias,
|
||||
estado: 'Ativo',
|
||||
};
|
||||
|
||||
const { data: estData, error: errE } = editandoEstagio
|
||||
? await supabase.from('estagios').update(payload).eq('id', editandoEstagio.id).select().single()
|
||||
: await supabase.from('estagios').insert([payload]).select().single();
|
||||
|
||||
if (errE) { setLoading(false); return Alert.alert("Erro", errE.message); }
|
||||
|
||||
const currentId = editandoEstagio?.id || estData.id;
|
||||
await supabase.from('horarios_estagio').delete().eq('estagio_id', currentId);
|
||||
|
||||
const horarios = [];
|
||||
if (hManhaIni && hManhaFim) horarios.push({ estagio_id: currentId, periodo: 'Manhã', hora_inicio: hManhaIni, hora_fim: hManhaFim });
|
||||
if (hTardeIni && hTardeFim) horarios.push({ estagio_id: currentId, periodo: 'Tarde', hora_inicio: hTardeIni, hora_fim: hTardeFim });
|
||||
|
||||
if (horarios.length > 0) await supabase.from('horarios_estagio').insert(horarios);
|
||||
|
||||
handleFecharModal();
|
||||
fetchDados();
|
||||
};
|
||||
|
||||
const handleFecharModal = () => {
|
||||
setModalVisible(false);
|
||||
setEditandoEstagio(null);
|
||||
setAlunoSelecionado(null);
|
||||
setEmpresaSelecionada(null);
|
||||
setDataInicio(''); setDataFim('');
|
||||
setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
|
||||
setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
|
||||
setDataInicio(''); setDataFim(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
|
||||
setPasso(1);
|
||||
};
|
||||
|
||||
// --- Filtros ---
|
||||
const estagiosFiltrados = useMemo(() => {
|
||||
const groups: Record<string, Estagio[]> = {};
|
||||
estagios.filter(e => e.alunos?.nome?.toLowerCase().includes(searchMain.toLowerCase())).forEach(e => {
|
||||
const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Sem Turma';
|
||||
const chave = e.alunos ? `${e.alunos.ano}º ${e.alunos.turma_curso}` : 'Geral';
|
||||
if (!groups[chave]) groups[chave] = [];
|
||||
groups[chave].push(e);
|
||||
});
|
||||
return Object.keys(groups).map(titulo => ({ titulo, dados: groups[titulo] })).sort((a, b) => b.titulo.localeCompare(a.titulo));
|
||||
}, [estagios, searchMain]);
|
||||
|
||||
const alunosAgrupados = useMemo(() => {
|
||||
const groups: Record<string, Aluno[]> = {};
|
||||
alunos.forEach(a => {
|
||||
const key = `${a.ano}º ${a.turma_curso}`.trim();
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(a);
|
||||
});
|
||||
return groups;
|
||||
}, [alunos]);
|
||||
|
||||
const empresasAgrupadas = useMemo(() => {
|
||||
const groups: Record<string, Empresa[]> = {};
|
||||
empresas.forEach(e => {
|
||||
const curso = (e.curso || 'Geral').trim().toUpperCase();
|
||||
if (!groups[curso]) groups[curso] = [];
|
||||
groups[curso].push(e);
|
||||
});
|
||||
return groups;
|
||||
}, [empresas]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
{/* Header */}
|
||||
|
||||
{/* HEADER EPVC */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto}/>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul}/>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Estágios</Text>
|
||||
<TouchableOpacity style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]} onPress={fetchDados}>
|
||||
<Ionicons name="refresh" size={20} color={cores.azul}/>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Plano de Estágios</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>{estagios.length} Alunos Colocados</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.btnAction, { borderColor: cores.borda }]} onPress={fetchDados}>
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul}/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]}>
|
||||
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: 120 }]} showsVerticalScrollIndicator={false}>
|
||||
{/* SEARCH MODERNO */}
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.secundario} />
|
||||
<TextInput placeholder="Procurar estagiário..." placeholderTextColor={cores.secundario} style={[styles.searchInput, { color: cores.texto }]} onChangeText={setSearchMain} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<TextInput
|
||||
placeholder="Pesquisar por aluno ou turma..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
onChangeText={setSearchMain}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{estagiosFiltrados.map(grupo => (
|
||||
{loading ? (
|
||||
<ActivityIndicator size="large" color={cores.azul} style={{marginTop: 50}} />
|
||||
) : estagiosFiltrados.map(grupo => (
|
||||
<View key={grupo.titulo} style={{ marginBottom: 25 }}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.sectionIndicator, { backgroundColor: cores.azul }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{grupo.titulo}</Text>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{grupo.titulo}</Text>
|
||||
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
|
||||
</View>
|
||||
|
||||
|
||||
{grupo.dados.map(e => (
|
||||
<View key={e.id} style={[styles.estagioCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
<TouchableOpacity
|
||||
key={e.id}
|
||||
activeOpacity={0.8}
|
||||
style={[styles.estagioCard, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => {
|
||||
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 || '');
|
||||
carregarHorariosEdicao(e.id);
|
||||
setPasso(2); setModalVisible(true);
|
||||
}}
|
||||
onLongPress={() => prepararApagar(e.id, e.alunos?.nome)}
|
||||
>
|
||||
onLongPress={() => {
|
||||
setEstagioParaApagar({ id: e.id, nome: e.alunos?.nome });
|
||||
setDeleteModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>{e.alunos?.nome.charAt(0)}</Text>
|
||||
</View>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{e.alunos?.nome}</Text>
|
||||
<Text style={[styles.turmaNome, { color: cores.secundario }]}>{e.alunos?.turma_curso}</Text>
|
||||
<View style={styles.empresaRow}>
|
||||
<Ionicons name="business-outline" size={13} color={cores.laranja} />
|
||||
<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>
|
||||
<Ionicons name="ellipsis-vertical" size={18} color={cores.secundario} />
|
||||
</View>
|
||||
<View style={styles.cardDetails}>
|
||||
<View style={styles.detailItem}><Ionicons name="business" size={14} color={cores.azul} /><Text style={[styles.detailText, { color: cores.texto }]} numberOfLines={1}>{e.empresas?.nome}</Text></View>
|
||||
<View style={styles.detailItem}><Ionicons name="time" size={14} color={cores.azul} /><Text style={[styles.detailText, { color: cores.texto }]}>{e.horas_diarias}/dia</Text></View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]} onPress={() => { setPasso(1); setModalVisible(true); }}>
|
||||
<Ionicons name="add" size={30} color="#fff" />
|
||||
<Text style={styles.fabText}>Novo Estágio</Text>
|
||||
{/* FAB EPVC */}
|
||||
<TouchableOpacity
|
||||
style={[styles.fab, { backgroundColor: cores.laranja, bottom: insets.bottom + 20 }]}
|
||||
onPress={() => { setPasso(1); setModalVisible(true); }}
|
||||
>
|
||||
<Ionicons name="add" size={28} color="#fff" />
|
||||
<Text style={styles.fabText}>Novo Plano</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
|
||||
{/* --- Modal Principal (Cadastro/Edição) --- */}
|
||||
{/* MODAL DE CRIAÇÃO/EDIÇÃO */}
|
||||
<Modal visible={modalVisible} animationType="slide" transparent>
|
||||
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card, paddingBottom: insets.bottom + 20 }]}>
|
||||
<View style={styles.modalIndicator} />
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>{passo === 1 ? "Vincular Dados" : "Detalhes do Estágio"}</Text>
|
||||
<TouchableOpacity onPress={handleFecharModal} style={[styles.closeBtn, {backgroundColor: cores.fundo}]}><Ionicons name="close" size={22} color={cores.texto} /></TouchableOpacity>
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>{passo === 1 ? "Vinculação" : "Configuração"}</Text>
|
||||
<Text style={[styles.modalSub, { color: cores.laranja }]}>Passo {passo} de 2</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleFecharModal} style={[styles.closeBtn, {backgroundColor: cores.fundo}]}>
|
||||
<Ionicons name="close" size={22} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false} nestedScrollEnabled>
|
||||
{passo === 1 ? (
|
||||
<View style={{ gap: 20 }}>
|
||||
<Text style={styles.inputLabel}>Estagiário</Text>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 200 }]}>
|
||||
<Text style={styles.inputLabel}>Selecionar Estagiário</Text>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 220 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{Object.entries(alunosAgrupados).map(([turma, lista]) => (
|
||||
{Object.entries(alunos.reduce((acc, a) => {
|
||||
const key = `${a.ano}º ${a.turma_curso}`.trim().toUpperCase();
|
||||
if (!acc[key]) acc[key] = []; acc[key].push(a); return acc;
|
||||
}, {} as Record<string, Aluno[]>))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([turma, lista]) => (
|
||||
<View key={turma}>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.azulSuave }]}><Text style={[styles.groupHeaderText, { color: cores.azul }]}>{turma}</Text></View>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{turma}</Text></View>
|
||||
{lista.map(a => (
|
||||
<TouchableOpacity key={a.id} style={[styles.pickerItem, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]} onPress={() => setAlunoSelecionado(a)}>
|
||||
<TouchableOpacity
|
||||
key={a.id}
|
||||
style={[styles.pickerItem, alunoSelecionado?.id === a.id && { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => setAlunoSelecionado(a)}
|
||||
>
|
||||
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{a.nome}</Text>
|
||||
{alunoSelecionado?.id === a.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -304,15 +329,25 @@ export default function Estagios() {
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<Text style={styles.inputLabel}>Empresa</Text>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 200 }]}>
|
||||
<Text style={styles.inputLabel}>Entidade de Acolhimento</Text>
|
||||
<View style={[styles.pickerContainer, { borderColor: cores.borda, maxHeight: 220 }]}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
{Object.entries(empresasAgrupadas).map(([curso, lista]) => (
|
||||
{Object.entries(empresas.reduce((acc, e) => {
|
||||
const key = (e.curso || 'GERAL').trim().toUpperCase();
|
||||
if (!acc[key]) acc[key] = []; acc[key].push(e); return acc;
|
||||
}, {} as Record<string, Empresa[]>))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([curso, lista]) => (
|
||||
<View key={curso}>
|
||||
<View style={[styles.groupHeader, { backgroundColor: cores.fundo }]}><Text style={[styles.groupHeaderText, { color: cores.secundario }]}>{curso}</Text></View>
|
||||
{lista.map(emp => (
|
||||
<TouchableOpacity key={emp.id} style={[styles.pickerItem, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]} onPress={() => setEmpresaSelecionada(emp)}>
|
||||
<TouchableOpacity
|
||||
key={emp.id}
|
||||
style={[styles.pickerItem, empresaSelecionada?.id === emp.id && { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => setEmpresaSelecionada(emp)}
|
||||
>
|
||||
<Text style={[styles.pickerItemText, { color: cores.texto }]}>{emp.nome}</Text>
|
||||
{empresaSelecionada?.id === emp.id && <Ionicons name="checkmark-circle" size={18} color={cores.azul} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -323,69 +358,75 @@ export default function Estagios() {
|
||||
) : (
|
||||
<View style={{ gap: 18 }}>
|
||||
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Text style={styles.groupTitle}>Duração</Text>
|
||||
<Text style={[styles.groupTitle, { color: cores.azul }]}>Cronograma</Text>
|
||||
<View style={styles.rowInputs}>
|
||||
<View style={{flex:1}}><Text style={styles.miniLabel}>INÍCIO</Text><TextInput style={[styles.modernInput, { color: cores.texto }]} value={dataInicio} onChangeText={setDataInicio} placeholder="YYYY-MM-DD"/></View>
|
||||
<View style={{flex:1}}><Text style={styles.miniLabel}>FIM</Text><TextInput style={[styles.modernInput, { color: cores.texto }]} value={dataFim} onChangeText={setDataFim} placeholder="YYYY-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}}><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>
|
||||
</View>
|
||||
|
||||
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Text style={styles.groupTitle}>Horários</Text>
|
||||
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
|
||||
<Text style={[styles.groupTitle, { color: cores.azul }]}>Horário de Trabalho</Text>
|
||||
<View style={[styles.totalBadge, { backgroundColor: cores.laranja }]}><Text style={styles.totalText}>{totalHorasDiarias}/dia</Text></View>
|
||||
</View>
|
||||
<View style={styles.tabelaRow}>
|
||||
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Manhã</Text>
|
||||
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hManhaIni} onChangeText={setHManhaIni} placeholder="09:00" />
|
||||
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hManhaFim} onChangeText={setHManhaFim} placeholder="13:00" />
|
||||
<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" />
|
||||
</View>
|
||||
<View style={styles.tabelaRow}>
|
||||
<Text style={[styles.tabelaLabel, {color: cores.texto}]}>Tarde</Text>
|
||||
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hTardeIni} onChangeText={setHTardeIni} placeholder="14:00" />
|
||||
<TextInput style={[styles.tabelaInput, {color: cores.texto}]} value={hTardeFim} onChangeText={setHTardeFim} placeholder="18:00" />
|
||||
<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" />
|
||||
</View>
|
||||
<View style={[styles.totalBadge, { backgroundColor: cores.azul }]}><Text style={styles.totalText}>Total: {totalHorasDiarias}/dia</Text></View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Text style={styles.groupTitle}>Tutor</Text>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholder="Nome do Tutor"/>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, marginTop: 10}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Contacto"/>
|
||||
<Text style={[styles.groupTitle, { color: cores.azul }]}>Responsável na Empresa</Text>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, backgroundColor: cores.card}]} value={empresaSelecionada?.tutor_nome} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_nome: t} : p)} placeholder="Nome do Tutor"/>
|
||||
<TextInput style={[styles.modernInput, {color: cores.texto, backgroundColor: cores.card, marginTop: 10}]} value={empresaSelecionada?.tutor_telefone} onChangeText={(t) => setEmpresaSelecionada(p => p ? {...p, tutor_telefone: t} : p)} keyboardType="phone-pad" placeholder="Telemóvel"/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.modalFooter}>
|
||||
{passo === 2 && <TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { backgroundColor: cores.fundo }]}><Text style={{color: cores.texto, fontWeight: '700'}}>VOLTAR</Text></TouchableOpacity>}
|
||||
{passo === 2 && (
|
||||
<TouchableOpacity onPress={() => setPasso(1)} style={[styles.btnModalSec, { borderColor: cores.borda }]}>
|
||||
<Text style={{color: cores.texto, fontWeight: '800'}}>VOLTAR</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if(passo === 1) {
|
||||
if(alunoSelecionado && empresaSelecionada) setPasso(2);
|
||||
else Alert.alert("Vai dar merda", "Seleciona o aluno e a empresa primeiro!");
|
||||
else Alert.alert("Atenção", "Selecione o aluno e a empresa.");
|
||||
} else salvarEstagio();
|
||||
}}
|
||||
style={[styles.btnModalPri, { backgroundColor: cores.azul }]}
|
||||
>
|
||||
<Text style={{color:'#fff', fontWeight: '900'}}>{passo === 1 ? "PRÓXIMO" : "FINALIZAR"}</Text>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={{color:'#fff', fontWeight: '900'}}>{passo === 1 ? "CONTINUAR" : "FINALIZAR"}</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* --- NOVO MODAL DE ELIMINAÇÃO MODERNIZADO --- */}
|
||||
{/* DELETE MODAL */}
|
||||
<Modal visible={deleteModalVisible} transparent animationType="fade">
|
||||
<View style={[styles.modalOverlay, { backgroundColor: 'rgba(0,0,0,0.7)' }]}>
|
||||
<View style={[styles.deleteCard, { backgroundColor: cores.card }]}>
|
||||
<View style={[styles.deleteIconBg, { backgroundColor: cores.vermelho + '20' }]}>
|
||||
<Ionicons name="trash-bin" size={32} color={cores.vermelho} />
|
||||
<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>
|
||||
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Eliminar Estágio?</Text>
|
||||
<Text style={[styles.deleteTitle, { color: cores.texto }]}>Cancelar Estágio?</Text>
|
||||
<Text style={[styles.deleteSubtitle, { color: cores.secundario }]}>
|
||||
Estás prestes a apagar o registo de <Text style={{fontWeight: '900', color: cores.texto}}>{estagioParaApagar?.nome}</Text>. Esta ação é irreversível.
|
||||
Irá remover o vínculo de <Text style={{fontWeight: '900', color: cores.texto}}>{estagioParaApagar?.nome}</Text>. Esta ação é irreversível.
|
||||
</Text>
|
||||
<View style={styles.deleteFooter}>
|
||||
<TouchableOpacity style={[styles.deleteBtnCancel, { backgroundColor: cores.fundo }]} onPress={() => setDeleteModalVisible(false)}>
|
||||
<Text style={[styles.deleteBtnText, { color: cores.texto }]}>CANCELAR</Text>
|
||||
<Text style={[styles.deleteBtnText, { color: cores.texto }]}>FECHAR</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.deleteBtnConfirm, { backgroundColor: cores.vermelho }]} onPress={confirmarEliminacao}>
|
||||
<Text style={[styles.deleteBtnText, { color: '#fff' }]}>ELIMINAR</Text>
|
||||
@@ -399,59 +440,60 @@ export default function Estagios() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900' },
|
||||
btnAction: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 52, borderRadius: 16, borderWidth: 1, marginBottom: 20 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 15, marginLeft: 5 },
|
||||
sectionIndicator: { width: 4, height: 16, borderRadius: 2, marginRight: 8 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
estagioCard: { padding: 16, borderRadius: 24, borderWidth: 1, marginBottom: 12 },
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 12 },
|
||||
avatar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 15, fontWeight: '800' },
|
||||
turmaNome: { fontSize: 12, fontWeight: '600' },
|
||||
cardDetails: { flexDirection: 'row', gap: 15 },
|
||||
detailItem: { flexDirection: 'row', alignItems: 'center', gap: 6, flexShrink: 1 },
|
||||
detailText: { fontSize: 12, fontWeight: '700' },
|
||||
fab: { position: 'absolute', right: 20, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 15, borderRadius: 20, elevation: 8 },
|
||||
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 8 },
|
||||
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
modalContent: { borderTopLeftRadius: 35, borderTopRightRadius: 35, padding: 25, height: '90%', width: '100%', marginTop: 'auto' },
|
||||
modalIndicator: { width: 40, height: 5, backgroundColor: '#E2E8F0', borderRadius: 10, alignSelf: 'center', marginBottom: 15 },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 20, fontWeight: '900' },
|
||||
closeBtn: { width: 38, height: 38, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
inputLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 8, marginLeft: 5 },
|
||||
pickerContainer: { borderWidth: 1, borderRadius: 20, overflow: 'hidden' },
|
||||
groupHeader: { paddingVertical: 8, paddingHorizontal: 15, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
|
||||
groupHeaderText: { fontSize: 10, fontWeight: '900' },
|
||||
pickerItem: { padding: 16, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
|
||||
pickerItemText: { fontSize: 14, fontWeight: '600' },
|
||||
modernGroup: { padding: 18, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
|
||||
groupTitle: { fontSize: 13, fontWeight: '900', color: '#2390a6', marginBottom: 10 },
|
||||
rowInputs: { flexDirection: 'row', gap: 12 },
|
||||
miniLabel: { fontSize: 10, fontWeight: '800', color: '#94A3B8', marginBottom: 5 },
|
||||
modernInput: { paddingVertical: 10, paddingHorizontal: 15, borderRadius: 12, backgroundColor: 'rgba(0,0,0,0.03)', fontSize: 14, fontWeight: '700' },
|
||||
tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
||||
tabelaLabel: { flex: 1.2, fontSize: 13, fontWeight: '700' },
|
||||
tabelaInput: { flex: 1, padding: 12, borderRadius: 12, backgroundColor: 'rgba(0,0,0,0.04)', textAlign: 'center', fontSize: 14, fontWeight: '700' },
|
||||
totalBadge: { alignSelf: 'flex-start', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, marginTop: 5 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5, marginBottom: 25 },
|
||||
searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 },
|
||||
sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 },
|
||||
sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 },
|
||||
estagioCard: { padding: 18, borderRadius: 28, borderWidth: 1, marginBottom: 14, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 },
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 14 },
|
||||
avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 20, fontWeight: '900' },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
empresaRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 },
|
||||
empresaText: { fontSize: 13, fontWeight: '600' },
|
||||
timeBadge: { flexDirection: 'row', alignItems: 'center', gap: 5, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12, borderWidth: 1 },
|
||||
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' },
|
||||
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 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
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' },
|
||||
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)' },
|
||||
pickerItemText: { fontSize: 15, fontWeight: '700' },
|
||||
modernGroup: { padding: 20, borderRadius: 30, borderWidth: 1, marginBottom: 18 },
|
||||
groupTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, letterSpacing: 0.5 },
|
||||
rowInputs: { flexDirection: 'row', gap: 15 },
|
||||
miniLabel: { fontSize: 9, fontWeight: '900', color: '#94A3B8', marginBottom: 6, marginLeft: 2 },
|
||||
modernInput: { paddingVertical: 14, paddingHorizontal: 16, borderRadius: 16, fontSize: 14, fontWeight: '700', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
|
||||
tabelaRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 },
|
||||
tabelaLabel: { flex: 1, fontSize: 14, fontWeight: '800' },
|
||||
tabelaInput: { flex: 1, padding: 14, borderRadius: 16, textAlign: 'center', fontSize: 14, fontWeight: '800', borderWidth: 1, borderColor: 'rgba(0,0,0,0.03)' },
|
||||
totalBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
totalText: { color: '#fff', fontSize: 11, fontWeight: '900' },
|
||||
modalFooter: { flexDirection: 'row', gap: 15, marginTop: 10 },
|
||||
btnModalPri: { flex: 2, height: 58, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
btnModalSec: { flex: 1, height: 58, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
// Estilos do Delete Modal
|
||||
deleteCard: { width: '85%', borderRadius: 30, padding: 25, alignItems: 'center' },
|
||||
deleteIconBg: { width: 70, height: 70, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
deleteTitle: { fontSize: 20, fontWeight: '900', marginBottom: 10 },
|
||||
deleteSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 20, marginBottom: 25 },
|
||||
deleteFooter: { flexDirection: 'row', gap: 12, width: '100%' },
|
||||
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' },
|
||||
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 },
|
||||
deleteFooter: { flexDirection: 'row', gap: 15, width: '100%' },
|
||||
deleteBtnCancel: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnConfirm: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
deleteBtnText: { fontSize: 13, fontWeight: '900' },
|
||||
deleteBtnText: { fontSize: 14, fontWeight: '900' },
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
// app/(Admin)/FaltasAlunos.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -37,7 +38,6 @@ interface Falta {
|
||||
const FaltasAlunos = memo(() => {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]);
|
||||
@@ -48,32 +48,23 @@ const FaltasAlunos = memo(() => {
|
||||
const [faltas, setFaltas] = useState<Falta[]>([]);
|
||||
const [loadingFaltas, setLoadingFaltas] = useState(false);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azul: azulPetroleo,
|
||||
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',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => { fetchAlunos(); }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.alunoId && typeof params.alunoId === 'string') {
|
||||
const alunoAuto = {
|
||||
id: params.alunoId,
|
||||
nome: (params.nome as string) || 'Aluno',
|
||||
n_escola: '',
|
||||
turma: '',
|
||||
};
|
||||
abrirFaltas(alunoAuto);
|
||||
}
|
||||
}, [params.alunoId]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -89,47 +80,85 @@ const FaltasAlunos = memo(() => {
|
||||
data?.forEach(item => {
|
||||
const nomeTurma = `${item.ano}º ${item.turma_curso}`;
|
||||
if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = [];
|
||||
agrupadas[nomeTurma].push({ id: item.id, nome: item.nome, n_escola: item.n_escola, turma: nomeTurma });
|
||||
agrupadas[nomeTurma].push({
|
||||
id: item.id,
|
||||
nome: item.nome,
|
||||
n_escola: item.n_escola,
|
||||
turma: nomeTurma
|
||||
});
|
||||
});
|
||||
|
||||
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
|
||||
} catch (err) { console.error(err); } finally { setLoading(false); }
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const abrirFaltas = async (aluno: Aluno) => {
|
||||
setAlunoSelecionado(aluno);
|
||||
setModalFaltasVisible(true);
|
||||
setLoadingFaltas(true);
|
||||
setFaltas([]);
|
||||
|
||||
try {
|
||||
const hoje = new Date().toISOString().split('T')[0];
|
||||
|
||||
// CORREÇÃO DO ERRO PGRST116: Usamos .limit(1) em vez de .single()
|
||||
const { data: listaEstagios, error: errEstagio } = await supabase
|
||||
.from('estagios')
|
||||
.select('data_inicio, data_fim')
|
||||
.lte('data_inicio', hoje)
|
||||
.gte('data_fim', hoje)
|
||||
.limit(1); // Pega apenas o primeiro se houver múltiplos
|
||||
|
||||
if (errEstagio) throw errEstagio;
|
||||
|
||||
// Se não houver estágio hoje, tentamos pegar o mais recente
|
||||
let inicio = '2026-01-01';
|
||||
let fim = '2026-12-31';
|
||||
|
||||
if (listaEstagios && listaEstagios.length > 0) {
|
||||
inicio = listaEstagios[0].data_inicio;
|
||||
fim = listaEstagios[0].data_fim;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('presencas')
|
||||
.select('id, data, justificacao_url, estado')
|
||||
.eq('aluno_id', aluno.id)
|
||||
.eq('estado', 'faltou')
|
||||
.gte('data', inicio)
|
||||
.lte('data', fim)
|
||||
.order('data', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setFaltas(data || []);
|
||||
} catch (err) { console.error(err); } finally { setLoadingFaltas(false); }
|
||||
|
||||
} catch (err) {
|
||||
console.error("Erro na base:", err);
|
||||
Alert.alert("Erro", "Não foi possível carregar as faltas deste estágio.");
|
||||
} finally {
|
||||
setLoadingFaltas(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verDocumento = async (url: string) => {
|
||||
try {
|
||||
const supported = await Linking.canOpenURL(url);
|
||||
if (supported) {
|
||||
await Linking.openURL(url);
|
||||
} else {
|
||||
Alert.alert("Erro", "Não foi possível abrir o link do documento.");
|
||||
}
|
||||
await Linking.openURL(url);
|
||||
} catch (error) {
|
||||
Alert.alert("Erro", "URL inválida ou documento inacessível.");
|
||||
Alert.alert("Erro", "Não foi possível abrir o comprovativo.");
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTurmas = turmas
|
||||
.map(turma => ({
|
||||
...turma,
|
||||
alunos: turma.alunos.filter(a => a.nome.toLowerCase().includes(search.toLowerCase()) || a.n_escola.includes(search)),
|
||||
alunos: turma.alunos.filter(a =>
|
||||
a.nome.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.n_escola.includes(search)
|
||||
),
|
||||
}))
|
||||
.filter(t => t.alunos.length > 0);
|
||||
|
||||
@@ -139,20 +168,17 @@ const FaltasAlunos = memo(() => {
|
||||
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color={cores.texto} />
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Gestão de Faltas</Text>
|
||||
<View style={{ width: 42 }} />
|
||||
<Text style={[styles.topTitle, { color: cores.texto }]}>Gestão de Faltas</Text>
|
||||
<View style={{ width: 42 }} />
|
||||
</View>
|
||||
|
||||
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.secundario} />
|
||||
<View style={[styles.searchContainer, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<TextInput
|
||||
placeholder="Procurar aluno..."
|
||||
placeholder="Procurar aluno ou nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -173,25 +199,19 @@ const FaltasAlunos = memo(() => {
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.section}>
|
||||
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.turmaLabel, { color: cores.azul }]}>{item.nome}</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>{item.nome}</Text>
|
||||
{item.alunos.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
onPress={() => abrirFaltas(aluno)}
|
||||
>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>{aluno.nome.charAt(0)}</Text>
|
||||
<TouchableOpacity key={aluno.id} activeOpacity={0.7} style={[styles.card, { backgroundColor: cores.card }]} onPress={() => abrirFaltas(aluno)}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarText}>{aluno.nome.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>Nº {aluno.n_escola}</Text>
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>Nº Aluno: {aluno.n_escola}</Text>
|
||||
</View>
|
||||
<View style={[styles.alertIcon, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="alert-circle" size={18} color={cores.vermelho} />
|
||||
</View>
|
||||
<Ionicons name="alert-circle-outline" size={20} color={cores.vermelho} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -199,24 +219,16 @@ const FaltasAlunos = memo(() => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
visible={modalFaltasVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => setModalFaltasVisible(false)}
|
||||
>
|
||||
<Modal visible={modalFaltasVisible} animationType="slide" transparent onRequestClose={() => setModalFaltasVisible(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
|
||||
<View style={[styles.modalHeader, { borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
|
||||
<View style={[styles.modalHeader, { backgroundColor: cores.card }]}>
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Histórico de Faltas</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>{alunoSelecionado?.nome}</Text>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Faltas de Estágio</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.azul }]}>{alunoSelecionado?.nome}</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => setModalFaltasVisible(false)}
|
||||
style={[styles.closeBtn, { backgroundColor: cores.fundo }]}
|
||||
>
|
||||
<Ionicons name="close" size={22} color={cores.texto} />
|
||||
<TouchableOpacity onPress={() => setModalFaltasVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="close" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -226,41 +238,32 @@ const FaltasAlunos = memo(() => {
|
||||
<ScrollView contentContainerStyle={{ padding: 20 }} showsVerticalScrollIndicator={false}>
|
||||
{faltas.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="checkmark-circle-outline" size={48} color={cores.verde} style={{ opacity: 0.3 }} />
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Nenhuma falta registada.</Text>
|
||||
<View style={[styles.emptyIconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="checkmark-done" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem faltas no período atual</Text>
|
||||
</View>
|
||||
) : (
|
||||
faltas.map(f => (
|
||||
<View key={f.id} style={[styles.faltaCard, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<View style={styles.faltaHeader}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Ionicons name="calendar-outline" size={16} color={cores.azul} style={{ marginRight: 8 }} />
|
||||
<Text style={[styles.faltaData, { color: cores.texto }]}>
|
||||
{new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</Text>
|
||||
<View key={f.id} style={[styles.faltaCard, { backgroundColor: cores.card }]}>
|
||||
<View style={styles.faltaRow}>
|
||||
<View style={[styles.dateBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="calendar" size={18} color={cores.azul} />
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: f.justificacao_url ? cores.verde + '20' : cores.vermelho + '20' }]}>
|
||||
<Text style={[styles.statusText, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<Text style={[styles.faltaData, { color: cores.texto }]}>
|
||||
{new Date(f.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||
</Text>
|
||||
<Text style={[styles.faltaStatus, { color: f.justificacao_url ? cores.verde : cores.vermelho }]}>
|
||||
{f.justificacao_url ? 'Justificada' : 'Injustificada'}
|
||||
</Text>
|
||||
</View>
|
||||
{f.justificacao_url && (
|
||||
<TouchableOpacity style={[styles.viewBtn, { backgroundColor: cores.azul }]} onPress={() => verDocumento(f.justificacao_url!)}>
|
||||
<Ionicons name="eye-outline" size={18} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{f.justificacao_url ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
style={[styles.pdfBtn, { backgroundColor: cores.azulSuave }]}
|
||||
onPress={() => verDocumento(f.justificacao_url!)}
|
||||
>
|
||||
<Ionicons name="document-attach" size={18} color={cores.azul} />
|
||||
<Text style={[styles.pdfBtnText, { color: cores.azul }]}>Ver Comprovativo</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.noJustifContainer}>
|
||||
<Ionicons name="warning-outline" size={14} color={cores.vermelho} />
|
||||
<Text style={[styles.noJustifText, { color: cores.vermelho }]}>Aguardando justificação</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
@@ -276,44 +279,37 @@ const FaltasAlunos = memo(() => {
|
||||
export default FaltasAlunos;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 },
|
||||
headerFixed: { paddingHorizontal: 20, paddingBottom: 15 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 70 },
|
||||
backBtnPremium: {
|
||||
width: 42, height: 42, borderRadius: 14,
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
borderWidth: 1, elevation: 2, shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2
|
||||
},
|
||||
title: { fontSize: 22, fontWeight: '800' },
|
||||
searchBox: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 15, paddingHorizontal: 15, height: 50, marginTop: 10 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
headerFixed: { paddingHorizontal: 20, paddingBottom: 10 },
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', height: 60, marginTop: 10 },
|
||||
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
topTitle: { fontSize: 18, fontWeight: '800' },
|
||||
searchContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 50, borderRadius: 16, borderWidth: 1.5, marginTop: 10 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 14, fontWeight: '600' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 30, paddingTop: 10 },
|
||||
section: { marginBottom: 25 },
|
||||
turmaBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 },
|
||||
turmaLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.8 },
|
||||
card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 22, marginBottom: 10, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 },
|
||||
avatar: { width: 46, height: 46, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, fontWeight: '800' },
|
||||
sectionLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 5, letterSpacing: 1.2 },
|
||||
card: { flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 24, marginBottom: 12, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 },
|
||||
avatar: { width: 50, height: 50, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },
|
||||
avatarText: { color: '#fff', fontSize: 22, fontWeight: '800' },
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
nome: { fontSize: 16, fontWeight: '700' },
|
||||
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
|
||||
subText: { fontSize: 12, fontWeight: '600', marginTop: 3 },
|
||||
alertIcon: { width: 34, height: 34, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
|
||||
modalContent: { height: '85%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, borderBottomWidth: 1 },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' },
|
||||
modalContent: { height: '82%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25 },
|
||||
modalTitle: { fontSize: 20, fontWeight: '800' },
|
||||
modalSubtitle: { fontSize: 14, fontWeight: '600', marginTop: 2 },
|
||||
closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
faltaCard: { padding: 16, borderRadius: 20, marginBottom: 15, borderWidth: 1 },
|
||||
faltaHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
||||
faltaData: { fontSize: 15, fontWeight: '700' },
|
||||
statusBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
statusText: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase' },
|
||||
pdfBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: 12, padding: 12 },
|
||||
pdfBtnText: { marginLeft: 10, fontSize: 14, fontWeight: '700' },
|
||||
noJustifContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 5 },
|
||||
noJustifText: { marginLeft: 6, fontSize: 12, fontWeight: '600', fontStyle: 'italic' },
|
||||
emptyState: { alignItems: 'center', marginTop: 50 },
|
||||
emptyText: { textAlign: 'center', marginTop: 15, fontSize: 14, fontWeight: '600' }
|
||||
modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 },
|
||||
closeBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
faltaCard: { padding: 16, borderRadius: 22, marginBottom: 12, elevation: 1 },
|
||||
faltaRow: { flexDirection: 'row', alignItems: 'center' },
|
||||
dateBadge: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
faltaData: { fontSize: 15, fontWeight: '800' },
|
||||
faltaStatus: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 },
|
||||
viewBtn: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
emptyState: { alignItems: 'center', marginTop: 80 },
|
||||
emptyIconContainer: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
emptyText: { fontSize: 16, fontWeight: '700' }
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'expo-router';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
StatusBar,
|
||||
@@ -23,6 +24,7 @@ export interface Aluno {
|
||||
nome: string;
|
||||
n_escola: string;
|
||||
turma: string;
|
||||
tem_estagio?: boolean;
|
||||
}
|
||||
|
||||
interface TurmaAgrupada {
|
||||
@@ -40,16 +42,19 @@ const ListaAlunosProfessor = memo(() => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.12)' : '#F0F9FA',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
@@ -57,7 +62,14 @@ const ListaAlunosProfessor = memo(() => {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('alunos')
|
||||
.select('id, nome, n_escola, ano, turma_curso')
|
||||
.select(`
|
||||
id,
|
||||
nome,
|
||||
n_escola,
|
||||
ano,
|
||||
turma_curso,
|
||||
estagios(id)
|
||||
`)
|
||||
.order('ano', { ascending: false })
|
||||
.order('nome', { ascending: true });
|
||||
|
||||
@@ -66,17 +78,21 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
const agrupadas: Record<string, Aluno[]> = {};
|
||||
data.forEach(item => {
|
||||
const nomeTurma = `${item.ano}º ${item.turma_curso}`;
|
||||
const nomeTurma = `${item.ano}º ${item.turma_curso}`.trim().toUpperCase();
|
||||
if (!agrupadas[nomeTurma]) agrupadas[nomeTurma] = [];
|
||||
agrupadas[nomeTurma].push({
|
||||
id: item.id,
|
||||
nome: item.nome,
|
||||
n_escola: item.n_escola,
|
||||
turma: nomeTurma,
|
||||
tem_estagio: item.estagios && item.estagios.length > 0
|
||||
});
|
||||
});
|
||||
|
||||
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
|
||||
setTurmas(Object.keys(agrupadas)
|
||||
.sort((a, b) => b.localeCompare(a))
|
||||
.map(nome => ({ nome, alunos: agrupadas[nome] }))
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Erro:', err);
|
||||
} finally {
|
||||
@@ -110,37 +126,35 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
|
||||
{/* HEADER IGUAL ÀS EMPRESAS */}
|
||||
{/* HEADER EPVC STYLE */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Alunos</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
|
||||
Gestão de turmas e estágios
|
||||
</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>Gestão de Turmas</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.refreshBtn, { backgroundColor: cores.azul }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
onPress={fetchAlunos}
|
||||
>
|
||||
<Ionicons name="reload" size={22} color="#fff" />
|
||||
<Ionicons name="reload-outline" size={20} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* SEARCH BAR IGUAL ÀS EMPRESAS */}
|
||||
{/* SEARCH MODERNO */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.azul} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Pesquisar por nome ou nº..."
|
||||
placeholder="Pesquisar por aluno ou nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -149,20 +163,18 @@ const ListaAlunosProfessor = memo(() => {
|
||||
</View>
|
||||
|
||||
{loading && !refreshing ? (
|
||||
<View style={styles.loadingCenter}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
<ActivityIndicator size="large" color={cores.azul} style={{ marginTop: 50 }} />
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 20 }]}
|
||||
contentContainerStyle={[styles.listPadding, { paddingBottom: insets.bottom + 100 }]}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ marginBottom: 25 }}>
|
||||
{/* Cabeçalho da Secção (Turma) */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{item.nome}</Text>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
|
||||
</View>
|
||||
|
||||
@@ -176,19 +188,31 @@ const ListaAlunosProfessor = memo(() => {
|
||||
params: { alunoId: aluno.id }
|
||||
})}
|
||||
>
|
||||
<View style={[styles.alunoIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="person" size={22} color={cores.azul} />
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>
|
||||
{aluno.nome.charAt(0)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.alunoInfo}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<View style={styles.idRow}>
|
||||
<Ionicons name="id-card-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.idText, { color: cores.secundario }]}>Nº Escola: {aluno.n_escola}</Text>
|
||||
<Ionicons name="finger-print-outline" size={13} color={cores.secundario} />
|
||||
<Text style={[styles.idText, { color: cores.secundario }]}>Nº {aluno.n_escola}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.4} />
|
||||
{aluno.tem_estagio ? (
|
||||
<View style={[styles.statusBadge, { backgroundColor: cores.verde + '20' }]}>
|
||||
<Ionicons name="checkmark-circle" size={12} color={cores.verde} />
|
||||
<Text style={[styles.statusText, { color: cores.verde }]}>COLOCADO</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.statusBadge, { backgroundColor: cores.laranja + '20' }]}>
|
||||
<Ionicons name="alert-circle" size={12} color={cores.laranja} />
|
||||
<Text style={[styles.statusText, { color: cores.laranja }]}>PENDENTE</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -196,11 +220,20 @@ const ListaAlunosProfessor = memo(() => {
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="people-outline" size={60} color={cores.borda} />
|
||||
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '600' }}>Nenhum aluno encontrado.</Text>
|
||||
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '700' }}>Nenhum aluno encontrado.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* FAB */}
|
||||
<TouchableOpacity
|
||||
style={[styles.fab, { backgroundColor: cores.azul, bottom: insets.bottom + 20 }]}
|
||||
onPress={() => Alert.alert("EPVC", "Funcionalidade de registo de aluno em desenvolvimento.")}
|
||||
>
|
||||
<Ionicons name="person-add" size={24} color="#fff" />
|
||||
<Text style={styles.fabText}>Novo Aluno</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
@@ -208,26 +241,30 @@ const ListaAlunosProfessor = memo(() => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 13, fontWeight: '600' },
|
||||
refreshBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 5 },
|
||||
searchSection: { paddingHorizontal: 20, marginVertical: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 18, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' },
|
||||
loadingCenter: { marginTop: 50, alignItems: 'center' },
|
||||
emptyContainer: { marginTop: 80, alignItems: 'center' },
|
||||
listPadding: { paddingHorizontal: 20 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 15 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 },
|
||||
sectionLine: { flex: 1, height: 1, borderRadius: 1 },
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 },
|
||||
alunoIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
searchSection: { paddingHorizontal: 24, marginBottom: 10 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, marginLeft: 12, fontSize: 14, fontWeight: '700' },
|
||||
listPadding: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 10, marginBottom: 18 },
|
||||
sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.8 },
|
||||
sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 },
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 28, marginBottom: 12, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 10 },
|
||||
avatar: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, fontWeight: '900' },
|
||||
alunoInfo: { flex: 1, marginLeft: 15 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800' },
|
||||
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
idRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 3 },
|
||||
idText: { fontSize: 13, fontWeight: '600' },
|
||||
statusBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10 },
|
||||
statusText: { fontSize: 9, fontWeight: '900' },
|
||||
emptyContainer: { marginTop: 80, alignItems: 'center' },
|
||||
fab: { position: 'absolute', right: 24, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 22, paddingVertical: 16, borderRadius: 22, elevation: 8, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
fabText: { color: '#fff', fontSize: 15, fontWeight: '900', marginLeft: 10, textTransform: 'uppercase' },
|
||||
});
|
||||
|
||||
export default ListaAlunosProfessor;
|
||||
@@ -1,3 +1,4 @@
|
||||
// app/(Professor)/Presencas.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
@@ -31,14 +32,16 @@ const Presencas = memo(() => {
|
||||
const [turmas, setTurmas] = useState<{ nome: string; alunos: Aluno[] }[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Paleta de Cores Premium
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#FFFFFF' : '#1A1C1E',
|
||||
secundario: isDarkMode ? '#8E8E93' : '#6C757D',
|
||||
azul: '#2390a6', // O teu azul de estimação
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
borda: isDarkMode ? '#2C2C2E' : '#E9ECEF',
|
||||
sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -100,54 +103,65 @@ const Presencas = memo(() => {
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* HEADER PREMIUM */}
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
{/* HEADER DINÂMICO */}
|
||||
<View style={styles.headerContainer}>
|
||||
<View style={styles.topRow}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color={cores.texto} />
|
||||
<Ionicons name="arrow-back" size={20} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Presenças</Text>
|
||||
<View style={{ width: 42 }} />
|
||||
<View style={styles.titleWrapper}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Presenças</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>Lista de Alunos</Text>
|
||||
</View>
|
||||
<View style={{ width: 45 }} />
|
||||
</View>
|
||||
|
||||
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.secundario} />
|
||||
<View style={[styles.searchWrapper, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={18} color={cores.azul} />
|
||||
<TextInput
|
||||
placeholder="Procurar aluno ou Nº..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
style={[styles.inputSearch, { color: cores.texto }]}
|
||||
/>
|
||||
{search !== '' && (
|
||||
<TouchableOpacity onPress={() => setSearch('')}>
|
||||
<Ionicons name="close-circle" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.centered}>
|
||||
<View style={styles.loaderArea}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
<Text style={[styles.loaderText, { color: cores.secundario }]}>A carregar turma...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.section}>
|
||||
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.turmaLabel, { color: cores.azul }]}>
|
||||
{item.nome} • {item.alunos.length} Alunos
|
||||
</Text>
|
||||
<View style={styles.turmaSection}>
|
||||
<View style={styles.turmaHeader}>
|
||||
<View style={[styles.turmaDot, { backgroundColor: cores.azul }]} />
|
||||
<Text style={[styles.turmaTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
<View style={[styles.countBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.countText, { color: cores.azul }]}>{item.alunos.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.alunos.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.alunoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/Professor/Alunos/CalendarioPresencas',
|
||||
@@ -155,18 +169,23 @@ const Presencas = memo(() => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>
|
||||
<View style={[styles.avatarBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarChar, { color: cores.azul }]}>
|
||||
{aluno.nome.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>Ver registo de presenças</Text>
|
||||
<View style={styles.alunoInfo}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]} numberOfLines={1}>{aluno.nome}</Text>
|
||||
<View style={styles.alunoStatusRow}>
|
||||
<Ionicons name="calendar-outline" size={12} color={cores.secundario} />
|
||||
<Text style={[styles.alunoSub, { color: cores.secundario }]}> Registo de faltas</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
<View style={[styles.goCircle, { backgroundColor: cores.fundo }]}>
|
||||
<Ionicons name="chevron-forward" size={16} color={cores.azul} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -184,77 +203,83 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0,
|
||||
},
|
||||
headerFixed: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 15,
|
||||
headerContainer: {
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 20,
|
||||
paddingTop: 10,
|
||||
},
|
||||
topBar: {
|
||||
topRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: 70,
|
||||
marginBottom: 20,
|
||||
},
|
||||
backBtnPremium: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 14,
|
||||
btnBack: {
|
||||
width: 45,
|
||||
height: 45,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
title: { fontSize: 22, fontWeight: '800' },
|
||||
searchBox: {
|
||||
titleWrapper: { alignItems: 'center' },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
searchWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: 15,
|
||||
paddingHorizontal: 15,
|
||||
height: 50,
|
||||
marginTop: 10,
|
||||
borderRadius: 18,
|
||||
paddingHorizontal: 16,
|
||||
height: 54,
|
||||
},
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
|
||||
section: { marginBottom: 25 },
|
||||
turmaBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' },
|
||||
listContent: { paddingHorizontal: 24, paddingBottom: 40 },
|
||||
turmaSection: { marginBottom: 30 },
|
||||
turmaHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
paddingLeft: 4
|
||||
},
|
||||
turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 },
|
||||
turmaTitle: { fontSize: 16, fontWeight: '800', flex: 1 },
|
||||
countBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 10,
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
turmaLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8
|
||||
},
|
||||
card: {
|
||||
countText: { fontSize: 11, fontWeight: '900' },
|
||||
alunoCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 14,
|
||||
borderRadius: 22,
|
||||
marginBottom: 10,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 10,
|
||||
padding: 12,
|
||||
borderRadius: 24,
|
||||
marginBottom: 12,
|
||||
elevation: 4,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 12,
|
||||
},
|
||||
avatar: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: 15,
|
||||
avatarBox: {
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatarText: { fontSize: 18, fontWeight: '800' },
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
nome: { fontSize: 16, fontWeight: '700' },
|
||||
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarChar: { fontSize: 20, fontWeight: '900' },
|
||||
alunoInfo: { flex: 1, marginLeft: 16 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 },
|
||||
alunoStatusRow: { flexDirection: 'row', alignItems: 'center' },
|
||||
alunoSub: { fontSize: 13, fontWeight: '500' },
|
||||
goCircle: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
loaderText: { marginTop: 15, fontSize: 14, fontWeight: '600' },
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// app/(Professor)/SumariosAlunos.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
@@ -46,18 +47,17 @@ const SumariosAlunos = memo(() => {
|
||||
const [loadingSumarios, setLoadingSumarios] = useState(false);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F2F5F9',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#FFFFFF' : '#1A1C1E',
|
||||
secundario: isDarkMode ? '#8E8E93' : '#6C757D',
|
||||
azul: '#2390a6',
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
borda: isDarkMode ? '#2C2C2E' : '#E9ECEF',
|
||||
sombra: isDarkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.06)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlunos();
|
||||
}, []);
|
||||
useEffect(() => { fetchAlunos(); }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.alunoId && typeof params.alunoId === 'string') {
|
||||
@@ -95,11 +95,7 @@ const SumariosAlunos = memo(() => {
|
||||
});
|
||||
|
||||
setTurmas(Object.keys(agrupadas).map(nome => ({ nome, alunos: agrupadas[nome] })));
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar alunos:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) { console.error(err); } finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const abrirSumarios = async (aluno: Aluno) => {
|
||||
@@ -107,20 +103,37 @@ const SumariosAlunos = memo(() => {
|
||||
setModalVisible(true);
|
||||
setLoadingSumarios(true);
|
||||
try {
|
||||
const hoje = new Date().toISOString().split('T')[0];
|
||||
|
||||
// 1. Procurar o estágio ativo
|
||||
const { data: listaEstagios } = await supabase
|
||||
.from('estagios')
|
||||
.select('data_inicio, data_fim')
|
||||
.lte('data_inicio', hoje)
|
||||
.gte('data_fim', hoje)
|
||||
.limit(1);
|
||||
|
||||
let inicio = '2026-01-01';
|
||||
let fim = '2026-12-31';
|
||||
|
||||
if (listaEstagios && listaEstagios.length > 0) {
|
||||
inicio = listaEstagios[0].data_inicio;
|
||||
fim = listaEstagios[0].data_fim;
|
||||
}
|
||||
|
||||
// 2. Buscar sumários filtrados pelo período do estágio
|
||||
const { data, error } = await supabase
|
||||
.from('presencas')
|
||||
.select('id, data, sumario')
|
||||
.eq('aluno_id', aluno.id)
|
||||
.gte('data', inicio)
|
||||
.lte('data', fim)
|
||||
.not('sumario', 'is', null)
|
||||
.order('data', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setSumarios(data || []);
|
||||
} catch (err) {
|
||||
console.error('Erro sumários:', err);
|
||||
} finally {
|
||||
setLoadingSumarios(false);
|
||||
}
|
||||
} catch (err) { console.error(err); } finally { setLoadingSumarios(false); }
|
||||
};
|
||||
|
||||
const filteredTurmas = turmas
|
||||
@@ -136,61 +149,67 @@ const SumariosAlunos = memo(() => {
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
<View style={styles.headerContainer}>
|
||||
<View style={styles.topRow}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={[styles.backBtnPremium, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnBack, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color={cores.texto} />
|
||||
<Ionicons name="arrow-back" size={20} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Sumários</Text>
|
||||
<View style={{ width: 42 }} />
|
||||
<View style={styles.titleWrapper}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Sumários</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>Estágio Atual</Text>
|
||||
</View>
|
||||
<View style={{ width: 45 }} />
|
||||
</View>
|
||||
|
||||
<View style={[styles.searchBox, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.secundario} />
|
||||
<View style={[styles.searchWrapper, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search-outline" size={18} color={cores.azul} />
|
||||
<TextInput
|
||||
placeholder="Procurar aluno..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
style={[styles.inputSearch, { color: cores.texto }]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.centered}>
|
||||
<View style={styles.loaderArea}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredTurmas}
|
||||
keyExtractor={item => item.nome}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.section}>
|
||||
<View style={[styles.turmaBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.turmaLabel, { color: cores.azul }]}>{item.nome}</Text>
|
||||
<View style={styles.turmaSection}>
|
||||
<View style={styles.turmaHeader}>
|
||||
<View style={[styles.turmaDot, { backgroundColor: cores.azul }]} />
|
||||
<Text style={[styles.turmaTitle, { color: cores.texto }]}>{item.nome}</Text>
|
||||
</View>
|
||||
|
||||
{item.alunos.map(aluno => (
|
||||
<TouchableOpacity
|
||||
key={aluno.id}
|
||||
activeOpacity={0.7}
|
||||
style={[styles.card, { backgroundColor: cores.card }]}
|
||||
style={[styles.alunoCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}
|
||||
onPress={() => abrirSumarios(aluno)}
|
||||
>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarText, { color: cores.azul }]}>{aluno.nome.charAt(0)}</Text>
|
||||
<View style={[styles.avatarBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.avatarChar, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.nome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>Nº {aluno.n_escola}</Text>
|
||||
<View style={styles.alunoInfo}>
|
||||
<Text style={[styles.alunoNome, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Text style={[styles.alunoSub, { color: cores.secundario }]}>Nº {aluno.n_escola}</Text>
|
||||
</View>
|
||||
<View style={[styles.goCircle, { backgroundColor: cores.fundo }]}>
|
||||
<Ionicons name="reader-outline" size={16} color={cores.azul} />
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -198,46 +217,45 @@ const SumariosAlunos = memo(() => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<Modal visible={modalVisible} animationType="slide" transparent={true} onRequestClose={() => setModalVisible(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
|
||||
<View style={[styles.modalHeader, { borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.fundo }]}>
|
||||
<View style={[styles.modalHeader, { backgroundColor: cores.card }]}>
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Sumários</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>{alunoSelecionado?.nome}</Text>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Caderno de Sumários</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.azul }]}>{alunoSelecionado?.nome}</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => setModalVisible(false)}
|
||||
style={[styles.closeBtn, { backgroundColor: cores.fundo }]}
|
||||
>
|
||||
<Ionicons name="close" size={22} color={cores.texto} />
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={[styles.closeBtn, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="close" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{loadingSumarios ? (
|
||||
<ActivityIndicator style={{ marginTop: 50 }} color={cores.azul} />
|
||||
) : (
|
||||
<ScrollView contentContainerStyle={{ padding: 20 }} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView contentContainerStyle={styles.modalScroll} showsVerticalScrollIndicator={false}>
|
||||
{sumarios.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="document-text-outline" size={48} color={cores.secundario} style={{ opacity: 0.3 }} />
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>Sem registos de sumários.</Text>
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={[styles.emptyIconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="receipt-outline" size={40} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.emptyText, { color: cores.texto }]}>Sem sumários neste estágio</Text>
|
||||
<Text style={[styles.emptySub, { color: cores.secundario }]}>Não foram encontrados registos para o período atual.</Text>
|
||||
</View>
|
||||
) : (
|
||||
sumarios.map(s => (
|
||||
<View key={s.id} style={[styles.sumarioCard, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<View style={styles.sumarioHeader}>
|
||||
<Ionicons name="calendar-outline" size={14} color={cores.azul} />
|
||||
<Text style={[styles.sumarioData, { color: cores.azul }]}>
|
||||
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||
</Text>
|
||||
<View key={s.id} style={[styles.sumarioCard, { backgroundColor: cores.card, shadowColor: cores.sombra }]}>
|
||||
<View style={styles.sumarioHeaderRow}>
|
||||
<View style={[styles.dateBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.dateText, { color: cores.azul }]}>
|
||||
{new Date(s.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' })}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.sumarioLine} />
|
||||
</View>
|
||||
<View style={styles.sumarioBody}>
|
||||
<Text style={[styles.sumarioTexto, { color: cores.texto }]}>{s.sumario}</Text>
|
||||
</View>
|
||||
<Text style={[styles.sumarioTexto, { color: cores.texto }]}>{s.sumario}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
@@ -254,38 +272,43 @@ export default SumariosAlunos;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0 },
|
||||
headerFixed: { paddingHorizontal: 20, paddingBottom: 15 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', height: 70 },
|
||||
backBtnPremium: {
|
||||
width: 42, height: 42, borderRadius: 14,
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
borderWidth: 1, elevation: 2, shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2
|
||||
},
|
||||
title: { fontSize: 22, fontWeight: '800' },
|
||||
searchBox: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 15, paddingHorizontal: 15, height: 50, marginTop: 10 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '500' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 30 },
|
||||
section: { marginBottom: 25 },
|
||||
turmaBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 12 },
|
||||
turmaLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.8 },
|
||||
card: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 22, marginBottom: 10, elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.04, shadowRadius: 10 },
|
||||
avatar: { width: 46, height: 46, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, fontWeight: '800' },
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
nome: { fontSize: 16, fontWeight: '700' },
|
||||
subText: { fontSize: 12, marginTop: 2, fontWeight: '500' },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
|
||||
modalContent: { height: '85%', borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, borderBottomWidth: 1 },
|
||||
modalTitle: { fontSize: 20, fontWeight: '800' },
|
||||
modalSubtitle: { fontSize: 14, fontWeight: '600', marginTop: 2 },
|
||||
closeBtn: { width: 36, height: 36, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
sumarioCard: { padding: 16, borderRadius: 18, marginBottom: 15, borderWidth: 1 },
|
||||
sumarioHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 },
|
||||
sumarioData: { fontSize: 13, fontWeight: '800', marginLeft: 6 },
|
||||
sumarioTexto: { fontSize: 15, lineHeight: 22, fontWeight: '500' },
|
||||
emptyState: { alignItems: 'center', marginTop: 50 },
|
||||
emptyText: { textAlign: 'center', marginTop: 15, fontSize: 14, fontWeight: '600' }
|
||||
headerContainer: { paddingHorizontal: 24, paddingBottom: 20, paddingTop: 10 },
|
||||
topRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
btnBack: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
titleWrapper: { alignItems: 'center' },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 12, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
searchWrapper: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 18, paddingHorizontal: 16, height: 54 },
|
||||
inputSearch: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '600' },
|
||||
listContent: { paddingHorizontal: 24, paddingBottom: 40 },
|
||||
turmaSection: { marginBottom: 30 },
|
||||
turmaHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, paddingLeft: 4 },
|
||||
turmaDot: { width: 6, height: 6, borderRadius: 3, marginRight: 10 },
|
||||
turmaTitle: { fontSize: 16, fontWeight: '800' },
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 24, marginBottom: 12, elevation: 4, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 1, shadowRadius: 12 },
|
||||
avatarBox: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarChar: { fontSize: 20, fontWeight: '900' },
|
||||
alunoInfo: { flex: 1, marginLeft: 16 },
|
||||
alunoNome: { fontSize: 16, fontWeight: '700', marginBottom: 2 },
|
||||
alunoSub: { fontSize: 13, fontWeight: '500' },
|
||||
goCircle: { width: 32, height: 32, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
loaderArea: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'flex-end' },
|
||||
modalContent: { height: '88%', borderTopLeftRadius: 35, borderTopRightRadius: 35, overflow: 'hidden' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 25, elevation: 2 },
|
||||
modalTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSubtitle: { fontSize: 13, fontWeight: '700', marginTop: 2 },
|
||||
closeBtn: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
modalScroll: { padding: 20, paddingBottom: 50 },
|
||||
sumarioCard: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 1, shadowRadius: 6 },
|
||||
sumarioHeaderRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 15 },
|
||||
dateBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 10 },
|
||||
dateText: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase' },
|
||||
sumarioLine: { flex: 1, height: 1, backgroundColor: 'rgba(128,128,128,0.1)', marginLeft: 15 },
|
||||
sumarioBody: { paddingLeft: 4 },
|
||||
sumarioTexto: { fontSize: 15, lineHeight: 24, fontWeight: '500', letterSpacing: 0.2 },
|
||||
emptyContainer: { alignItems: 'center', marginTop: 100 },
|
||||
emptyIconBox: { width: 80, height: 80, borderRadius: 30, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
emptyText: { fontSize: 16, fontWeight: '700' },
|
||||
emptySub: { fontSize: 13, textAlign: 'center', marginTop: 8, opacity: 0.7, paddingHorizontal: 30 }
|
||||
});
|
||||
@@ -34,7 +34,8 @@ const DetalhesEmpresa = memo(() => {
|
||||
const params = useLocalSearchParams();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const empresaOriginal: Empresa = useMemo(() => {
|
||||
if (!params.empresa) return {} as Empresa;
|
||||
@@ -55,16 +56,17 @@ const DetalhesEmpresa = memo(() => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
|
||||
vermelhoSuave: '#FFF5F5',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
overlay: isDarkMode ? 'rgba(0,0,0,0.85)' : 'rgba(15, 23, 42, 0.4)',
|
||||
overlay: 'rgba(26, 54, 93, 0.8)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -119,7 +121,6 @@ const DetalhesEmpresa = memo(() => {
|
||||
router.back();
|
||||
} catch (e) {
|
||||
setShowDeleteModal(false);
|
||||
// Se tiver alunos ligados, vai dar merda ao apagar
|
||||
alert('Não é possível apagar empresas com estágios ativos.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -130,29 +131,27 @@ const DetalhesEmpresa = memo(() => {
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
{/* FEEDBACK TOAST */}
|
||||
{showSuccess && (
|
||||
<View style={[styles.toast, { backgroundColor: cores.azul, top: insets.top + 10 }]}>
|
||||
<Ionicons name="checkmark-circle" size={20} color="#fff" />
|
||||
<Text style={styles.toastText}>Alterações guardadas com sucesso!</Text>
|
||||
<Text style={styles.toastText}>Dados atualizados!</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* MODAL DE APAGAR MODERNO */}
|
||||
<Modal visible={showDeleteModal} transparent animationType="fade">
|
||||
<View style={[styles.modalOverlay, { backgroundColor: cores.overlay }]}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="trash" size={32} color={cores.vermelho} />
|
||||
<Ionicons name="warning" size={35} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Tens a certeza?</Text>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Eliminar Entidade?</Text>
|
||||
<Text style={[styles.modalSubtitle, { color: cores.secundario }]}>
|
||||
Esta ação irá remover permanentemente a entidade <Text style={{fontWeight: '800'}}>{empresaLocal.nome}</Text>.
|
||||
Esta ação é irreversível. A empresa <Text style={{fontWeight: '900', color: cores.texto}}>{empresaLocal.nome}</Text> será removida do sistema.
|
||||
</Text>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtn, { backgroundColor: cores.fundo }]}
|
||||
style={[styles.modalBtn, { backgroundColor: isDarkMode ? '#2D2D2D' : '#E2E8F0' }]}
|
||||
onPress={() => setShowDeleteModal(false)}
|
||||
>
|
||||
<Text style={[styles.modalBtnTxt, { color: cores.texto }]}>Cancelar</Text>
|
||||
@@ -162,7 +161,7 @@ const DetalhesEmpresa = memo(() => {
|
||||
style={[styles.modalBtn, { backgroundColor: cores.vermelho }]}
|
||||
onPress={confirmDelete}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.modalBtnTxt, { color: '#fff' }]}>Apagar</Text>}
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={[styles.modalBtnTxt, { color: '#fff' }]}>Confirmar</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -172,77 +171,84 @@ const DetalhesEmpresa = memo(() => {
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnAction, { borderColor: cores.borda }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Detalhes da Entidade</Text>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha Técnica</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>ID: #{empresaLocal.id?.slice(0, 5)}</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAction, { backgroundColor: editando ? cores.vermelho : cores.card, borderColor: editando ? cores.vermelho : cores.borda }]}
|
||||
style={[styles.btnAction, { backgroundColor: editando ? cores.laranja : 'transparent', borderColor: editando ? cores.laranja : cores.borda }]}
|
||||
onPress={() => {
|
||||
if(editando) setEmpresaLocal({...empresaOriginal});
|
||||
setEditando(!editando);
|
||||
}}
|
||||
>
|
||||
<Ionicons name={editando ? "close" : "pencil"} size={20} color={editando ? "#fff" : cores.azul} />
|
||||
<Ionicons name={editando ? "close" : "create-outline"} size={22} color={editando ? "#fff" : cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 20 }]} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView contentContainerStyle={[styles.scroll, { paddingBottom: insets.bottom + 30 }]} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="business" size={18} color={cores.azul} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Dados Gerais</Text>
|
||||
<View style={[styles.sideLine, { backgroundColor: cores.azul }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Informação Institucional</Text>
|
||||
</View>
|
||||
<ModernField label="Nome" value={empresaLocal.nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} />
|
||||
<ModernField label="Curso" value={empresaLocal.curso} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} />
|
||||
<ModernField label="Morada" value={empresaLocal.morada} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline />
|
||||
<ModernField label="Nome da Entidade" value={empresaLocal.nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, nome: v}))} cores={cores} icon="business-outline" />
|
||||
<ModernField label="Curso Associado" value={empresaLocal.curso} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, curso: v}))} cores={cores} icon="school-outline" />
|
||||
<ModernField label="Endereço Completo" value={empresaLocal.morada} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, morada: v}))} cores={cores} multiline icon="map-outline" />
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="person" size={18} color={cores.azul} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Contacto Tutor</Text>
|
||||
<View style={[styles.sideLine, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Gestão de Contacto</Text>
|
||||
</View>
|
||||
<ModernField label="Nome do Tutor" value={empresaLocal.tutor_nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} />
|
||||
<ModernField label="Telefone" value={empresaLocal.tutor_telefone} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" />
|
||||
<ModernField label="Tutor Responsável" value={empresaLocal.tutor_nome} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_nome: v}))} cores={cores} icon="person-outline" />
|
||||
<ModernField label="Contacto Telefónico" value={empresaLocal.tutor_telefone} editable={editando} onChangeText={(v: string) => setEmpresaLocal(p => ({...p, tutor_telefone: v}))} cores={cores} keyboardType="phone-pad" icon="call-outline" />
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="people" size={18} color={cores.azul} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>Alunos Ativos</Text>
|
||||
<View style={[styles.badge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.badgeTxt, { color: cores.azul }]}>{loadingAlunos ? '...' : alunos.length}</Text>
|
||||
<View style={[styles.sideLine, { backgroundColor: cores.secundario }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Alunos em Estágio</Text>
|
||||
<View style={[styles.countBadge, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.countBadgeTxt}>{loadingAlunos ? '-' : alunos.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{loadingAlunos ? <ActivityIndicator size="small" color={cores.azul} /> : alunos.length > 0 ? (
|
||||
alunos.map((aluno, i) => (
|
||||
<View key={aluno.id} style={[styles.alunoRow, i !== alunos.length - 1 && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.miniAvatar, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.miniAvatarTxt, { color: cores.azul }]}>{aluno.nome.charAt(0).toUpperCase()}</Text>
|
||||
<Ionicons name="person" size={14} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.alunoName, { color: cores.texto }]}>{aluno.nome}</Text>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.secundario} opacity={0.3} />
|
||||
</View>
|
||||
))
|
||||
) : <Text style={[styles.empty, { color: cores.secundario }]}>Sem alunos vinculados.</Text>}
|
||||
) : <Text style={[styles.empty, { color: cores.secundario }]}>Nenhum aluno vinculado a esta entidade.</Text>}
|
||||
</View>
|
||||
|
||||
<View style={{ marginTop: 5 }}>
|
||||
<View style={styles.footerActions}>
|
||||
{editando ? (
|
||||
<TouchableOpacity style={[styles.btnSave, { backgroundColor: cores.azul }]} onPress={handleSave} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : (
|
||||
<View style={styles.btnRow}><Ionicons name="cloud-done-outline" size={20} color="#fff" /><Text style={styles.btnSaveTxt}>Guardar Alterações</Text></View>
|
||||
<View style={styles.btnRow}>
|
||||
<Ionicons name="save-outline" size={20} color="#fff" />
|
||||
<Text style={styles.btnSaveTxt}>Confirmar Alterações</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity style={[styles.btnDel, { backgroundColor: cores.vermelhoSuave }]} onPress={() => setShowDeleteModal(true)}>
|
||||
<Ionicons name="trash-outline" size={20} color={cores.vermelho} />
|
||||
<Text style={[styles.btnDelTxt, { color: cores.vermelho }]}>Eliminar Entidade</Text>
|
||||
<TouchableOpacity style={[styles.btnDel, { borderColor: cores.vermelho }]} onPress={() => setShowDeleteModal(true)}>
|
||||
<Ionicons name="trash-bin-outline" size={18} color={cores.vermelho} />
|
||||
<Text style={[styles.btnDelTxt, { color: cores.vermelho }]}>Remover Parceiro</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -252,14 +258,22 @@ const DetalhesEmpresa = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const ModernField = ({ label, value, editable, cores, ...props }: any) => (
|
||||
const ModernField = ({ label, value, editable, cores, icon, ...props }: any) => (
|
||||
<View style={styles.fieldContainer}>
|
||||
<Text style={[styles.fieldLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<View style={styles.labelRow}>
|
||||
<Ionicons name={icon} size={12} color={cores.secundario} />
|
||||
<Text style={[styles.fieldLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
</View>
|
||||
{editable ? (
|
||||
<TextInput style={[styles.input, { color: cores.texto, backgroundColor: cores.fundo, borderColor: cores.azul }]} value={value} selectionColor={cores.azul} {...props} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: cores.texto, backgroundColor: cores.fundo, borderColor: cores.azul }]}
|
||||
value={value}
|
||||
selectionColor={cores.laranja}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.readOnly, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
|
||||
<Text style={[styles.readOnlyTxt, { color: cores.texto }]}>{value || '---'}</Text>
|
||||
<Text style={[styles.readOnlyTxt, { color: cores.texto }]}>{value || 'Não definido'}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -267,41 +281,43 @@ const ModernField = ({ label, value, editable, cores, ...props }: any) => (
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
toast: { position: 'absolute', left: 20, right: 20, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 15, borderRadius: 16, gap: 10, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10 },
|
||||
toastText: { color: '#fff', fontSize: 14, fontWeight: '800' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900' },
|
||||
btnAction: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 20, paddingTop: 10 },
|
||||
card: { padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 15 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 18 },
|
||||
sectionTitle: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1, marginLeft: 8, flex: 1 },
|
||||
fieldContainer: { marginBottom: 15 },
|
||||
fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', marginBottom: 5, marginLeft: 2 },
|
||||
input: { fontSize: 14, fontWeight: '700', paddingHorizontal: 12, paddingVertical: 10, borderRadius: 12, borderWidth: 1.5 },
|
||||
readOnly: { paddingHorizontal: 12, paddingVertical: 10, borderRadius: 12, borderWidth: 1 },
|
||||
readOnlyTxt: { fontSize: 14, fontWeight: '700' },
|
||||
badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6 },
|
||||
badgeTxt: { fontSize: 12, fontWeight: '900' },
|
||||
alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 },
|
||||
miniAvatar: { width: 30, height: 30, borderRadius: 8, justifyContent: 'center', alignItems: 'center', marginRight: 10 },
|
||||
miniAvatarTxt: { fontSize: 13, fontWeight: '900' },
|
||||
alunoName: { fontSize: 14, fontWeight: '700' },
|
||||
empty: { textAlign: 'center', fontSize: 12, fontWeight: '600', paddingVertical: 5 },
|
||||
btnSave: { height: 54, borderRadius: 16, justifyContent: 'center', alignItems: 'center', marginTop: 10 },
|
||||
btnRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
btnSaveTxt: { color: '#fff', fontSize: 15, fontWeight: '900', textTransform: 'uppercase' },
|
||||
btnDel: { height: 54, borderRadius: 16, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8 },
|
||||
btnDelTxt: { fontSize: 15, fontWeight: '800' },
|
||||
// Estilos do Modal
|
||||
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
|
||||
modalContent: { width: '100%', maxWidth: 340, borderRadius: 32, padding: 24, alignItems: 'center', borderWidth: 1, elevation: 20, shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 15 },
|
||||
iconCircle: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900', marginBottom: 10 },
|
||||
modalSubtitle: { fontSize: 14, textAlign: 'center', lineHeight: 20, marginBottom: 25 },
|
||||
modalButtons: { flexDirection: 'row', gap: 12, width: '100%' },
|
||||
modalBtn: { flex: 1, height: 50, borderRadius: 15, justifyContent: 'center', alignItems: 'center' },
|
||||
modalBtnTxt: { fontSize: 14, fontWeight: '800' }
|
||||
toast: { position: 'absolute', left: 25, right: 25, zIndex: 999, flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, gap: 12, elevation: 8 },
|
||||
toastText: { color: '#fff', fontSize: 14, fontWeight: '900' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
btnAction: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
scroll: { paddingHorizontal: 24, paddingTop: 10 },
|
||||
card: { padding: 22, borderRadius: 32, borderWidth: 1, marginBottom: 18, elevation: 2, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, gap: 10 },
|
||||
sideLine: { width: 4, height: 18, borderRadius: 2 },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
fieldContainer: { marginBottom: 18 },
|
||||
labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 6, marginLeft: 4 },
|
||||
fieldLabel: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
input: { fontSize: 15, fontWeight: '700', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 16, borderWidth: 1.5 },
|
||||
readOnly: { paddingHorizontal: 16, paddingVertical: 14, borderRadius: 16, borderWidth: 1 },
|
||||
readOnlyTxt: { fontSize: 15, fontWeight: '700' },
|
||||
countBadge: { width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' },
|
||||
countBadgeTxt: { color: '#fff', fontSize: 11, fontWeight: '900' },
|
||||
alunoRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14 },
|
||||
miniAvatar: { width: 32, height: 32, borderRadius: 10, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||
alunoName: { fontSize: 15, fontWeight: '700', flex: 1 },
|
||||
empty: { textAlign: 'center', fontSize: 13, fontWeight: '600', paddingVertical: 10, fontStyle: 'italic' },
|
||||
footerActions: { marginTop: 10 },
|
||||
btnSave: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', elevation: 5, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
btnRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
btnSaveTxt: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
btnDel: { height: 56, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 1.5, borderStyle: 'dashed' },
|
||||
btnDelTxt: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 25 },
|
||||
modalContent: { width: '100%', borderRadius: 40, padding: 30, alignItems: 'center', borderWidth: 1 },
|
||||
iconCircle: { width: 80, height: 80, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '900', marginBottom: 12 },
|
||||
modalSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22, marginBottom: 30 },
|
||||
modalButtons: { flexDirection: 'row', gap: 15 },
|
||||
modalBtn: { flex: 1, height: 55, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },
|
||||
modalBtnTxt: { fontSize: 15, fontWeight: '900' }
|
||||
});
|
||||
|
||||
export default DetalhesEmpresa;
|
||||
@@ -42,20 +42,21 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
// Estados do Formulário
|
||||
const [form, setForm] = useState({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
// Cores EPVC
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
branco: '#FFFFFF'
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchEmpresas = async () => {
|
||||
@@ -90,7 +91,7 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
);
|
||||
|
||||
const grupos = filtradas.reduce((acc: { [key: string]: Empresa[] }, empresa) => {
|
||||
const cursoKey = (empresa.curso || 'Outros').trim().toUpperCase();
|
||||
const cursoKey = (empresa.curso || 'Geral').trim().toUpperCase();
|
||||
if (!acc[cursoKey]) acc[cursoKey] = [];
|
||||
acc[cursoKey].push(empresa);
|
||||
return acc;
|
||||
@@ -104,7 +105,7 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
|
||||
const criarEmpresa = async () => {
|
||||
if (!form.nome || !form.morada || !form.tutorNome || !form.tutorTelefone || !form.curso) {
|
||||
Alert.alert('Atenção', 'Preenche todos os campos obrigatórios.');
|
||||
Alert.alert('Atenção', 'Preencha todos os campos.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
setEmpresas(prev => [...prev, data![0]]);
|
||||
setModalVisible(false);
|
||||
setForm({ nome: '', morada: '', tutorNome: '', tutorTelefone: '', curso: '' });
|
||||
Alert.alert('Sucesso', 'Entidade registada!');
|
||||
Alert.alert('Sucesso', 'Empresa registada com sucesso!');
|
||||
} catch (error: any) {
|
||||
Alert.alert('Erro', error.message);
|
||||
} finally {
|
||||
@@ -139,37 +140,37 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
|
||||
{/* HEADER MODERNIZADO */}
|
||||
{/* HEADER EPVC */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.backBtn, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.backBtn, { borderColor: cores.borda }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Empresas</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.secundario }]}>
|
||||
{empresas.length} entidades ativas
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Entidades</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: cores.laranja }]}>
|
||||
{empresas.length} parcerias ativas
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.addBtn, { backgroundColor: cores.azul }]}
|
||||
style={[styles.addBtn, { backgroundColor: cores.laranja }]}
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Ionicons name="add" size={28} color="#fff" />
|
||||
<Ionicons name="add" size={30} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* SEARCH BAR */}
|
||||
{/* PESQUISA */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={[styles.searchBar, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="search" size={20} color={cores.azul} />
|
||||
<Ionicons name="search-outline" size={20} color={cores.azul} />
|
||||
<TextInput
|
||||
style={[styles.searchInput, { color: cores.texto }]}
|
||||
placeholder="Pesquisar entidade ou curso..."
|
||||
placeholder="Procurar empresa ou curso..."
|
||||
placeholderTextColor={cores.secundario}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
@@ -190,7 +191,8 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={cores.azul} />}
|
||||
renderSectionHeader={({ section: { title } }) => (
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.secundario }]}>{title}</Text>
|
||||
<View style={[styles.sectionDot, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>{title}</Text>
|
||||
<View style={[styles.sectionLine, { backgroundColor: cores.borda }]} />
|
||||
</View>
|
||||
)}
|
||||
@@ -201,56 +203,58 @@ const ListaEmpresasProfessor = memo(() => {
|
||||
onPress={() => router.push({ pathname: '/Professor/Empresas/DetalhesEmpresa', params: { empresa: JSON.stringify(item) } })}
|
||||
>
|
||||
<View style={[styles.empresaIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="business" size={22} color={cores.azul} />
|
||||
<Ionicons name="business-outline" size={24} color={cores.azul} />
|
||||
</View>
|
||||
|
||||
<View style={styles.empresaInfo}>
|
||||
<Text style={[styles.empresaNome, { color: cores.texto }]}>{item.nome}</Text>
|
||||
<View style={styles.tutorRow}>
|
||||
<Ionicons name="person-circle-outline" size={14} color={cores.secundario} />
|
||||
<Text style={[styles.tutorText, { color: cores.secundario }]}>{item.tutor_nome}</Text>
|
||||
<Ionicons name="location-outline" size={12} color={cores.secundario} />
|
||||
<Text style={[styles.tutorText, { color: cores.secundario }]} numberOfLines={1}>{item.morada}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.4} />
|
||||
<View style={[styles.arrowCircle, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="business-outline" size={60} color={cores.borda} />
|
||||
<Text style={{ color: cores.secundario, marginTop: 10, fontWeight: '600' }}>Sem resultados.</Text>
|
||||
<Ionicons name="search-outline" size={60} color={cores.borda} />
|
||||
<Text style={{ color: cores.secundario, marginTop: 15, fontWeight: '700' }}>Nenhuma entidade encontrada.</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
|
||||
{/* MODAL DE CRIAÇÃO PREMIUM */}
|
||||
{/* MODAL NOVO REGISTO */}
|
||||
<Modal visible={modalVisible} animationType="slide" transparent>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalOverlay}>
|
||||
<View style={[styles.modalContent, { backgroundColor: cores.card }]}>
|
||||
<View style={styles.modalHeader}>
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Nova Empresa</Text>
|
||||
<Text style={[styles.modalSub, { color: cores.secundario }]}>Preenche os detalhes da entidade</Text>
|
||||
<Text style={[styles.modalTitle, { color: cores.texto }]}>Registar Parceiro</Text>
|
||||
<Text style={[styles.modalSub, { color: cores.laranja }]}>Nova entidade de estágio</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)} style={styles.closeBtn}>
|
||||
<Ionicons name="close" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 20 }}>
|
||||
<ModernInput label="Nome da Entidade" icon="business" value={form.nome} onChangeText={(v:any)=>setForm({...form, nome:v})} cores={cores} />
|
||||
<ModernInput label="Curso Principal" icon="school" value={form.curso} onChangeText={(v:any)=>setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI" />
|
||||
<ModernInput label="Morada / Sede" icon="location" value={form.morada} onChangeText={(v:any)=>setForm({...form, morada:v})} cores={cores} />
|
||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 30 }}>
|
||||
<ModernInput label="Nome da Empresa" icon="business" value={form.nome} onChangeText={(v:any)=>setForm({...form, nome:v})} cores={cores} />
|
||||
<ModernInput label="Curso Associado" icon="school" value={form.curso} onChangeText={(v:any)=>setForm({...form, curso:v})} cores={cores} placeholder="Ex: GPSI, MULT, etc." />
|
||||
<ModernInput label="Localização / Morada" icon="map" value={form.morada} onChangeText={(v:any)=>setForm({...form, morada:v})} cores={cores} />
|
||||
|
||||
<View style={styles.divider} />
|
||||
<View style={[styles.divider, { backgroundColor: cores.borda }]} />
|
||||
|
||||
<ModernInput label="Tutor Responsável" icon="person" value={form.tutorNome} onChangeText={(v:any)=>setForm({...form, tutorNome:v})} cores={cores} />
|
||||
<ModernInput label="Contacto Telefónico" icon="call" value={form.tutorTelefone} onChangeText={(v:any)=>setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} />
|
||||
<ModernInput label="Tutor na Empresa" icon="person" value={form.tutorNome} onChangeText={(v:any)=>setForm({...form, tutorNome:v})} cores={cores} />
|
||||
<ModernInput label="Contacto do Tutor" icon="call" value={form.tutorTelefone} onChangeText={(v:any)=>setForm({...form, tutorTelefone:v})} keyboardType="phone-pad" cores={cores} />
|
||||
|
||||
<TouchableOpacity style={[styles.saveBtn, { backgroundColor: cores.azul }]} onPress={criarEmpresa}>
|
||||
<Text style={styles.saveBtnText}>Registar Entidade</Text>
|
||||
<Text style={styles.saveBtnText}>Confirmar Registo</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
@@ -272,39 +276,41 @@ const ModernInput = ({ label, icon, cores, ...props }: any) => (
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
backBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
headerTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
headerSubtitle: { fontSize: 13, fontWeight: '600' },
|
||||
addBtn: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 5 },
|
||||
searchSection: { paddingHorizontal: 20, marginVertical: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 18, borderWidth: 1.5 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
headerTitle: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8 },
|
||||
headerSubtitle: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
addBtn: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 8 },
|
||||
searchSection: { paddingHorizontal: 24, marginVertical: 15 },
|
||||
searchBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 20, borderWidth: 1.5 },
|
||||
searchInput: { flex: 1, marginLeft: 10, fontSize: 15, fontWeight: '700' },
|
||||
loadingCenter: { marginTop: 50, alignItems: 'center' },
|
||||
emptyContainer: { marginTop: 80, alignItems: 'center' },
|
||||
listPadding: { paddingHorizontal: 20 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 25, marginBottom: 15 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginRight: 10 },
|
||||
sectionLine: { flex: 1, height: 1, borderRadius: 1 },
|
||||
empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 24, marginBottom: 12, borderWidth: 1, elevation: 2, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 10 },
|
||||
empresaIcon: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
loadingCenter: { marginTop: 60, alignItems: 'center' },
|
||||
emptyContainer: { marginTop: 100, alignItems: 'center' },
|
||||
listPadding: { paddingHorizontal: 24 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginTop: 30, marginBottom: 15 },
|
||||
sectionDot: { width: 8, height: 8, borderRadius: 4, marginRight: 10 },
|
||||
sectionTitle: { fontSize: 14, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
sectionLine: { flex: 1, height: 1, marginLeft: 15, opacity: 0.5 },
|
||||
empresaCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 28, marginBottom: 14, borderWidth: 1, elevation: 3, shadowColor: '#000', shadowOpacity: 0.04, shadowRadius: 12 },
|
||||
empresaIcon: { width: 50, height: 50, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
empresaInfo: { flex: 1, marginLeft: 15 },
|
||||
empresaNome: { fontSize: 16, fontWeight: '800' },
|
||||
tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 5, marginTop: 4 },
|
||||
tutorText: { fontSize: 13, fontWeight: '600' },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'flex-end' },
|
||||
modalContent: { borderTopLeftRadius: 35, borderTopRightRadius: 35, padding: 25, maxHeight: '85%' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 25 },
|
||||
modalTitle: { fontSize: 22, fontWeight: '900' },
|
||||
modalSub: { fontSize: 14, fontWeight: '600', marginTop: 2 },
|
||||
closeBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' },
|
||||
divider: { height: 1, backgroundColor: 'rgba(0,0,0,0.05)', marginVertical: 10 },
|
||||
inputWrapper: { marginBottom: 18 },
|
||||
inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 8, marginLeft: 5, letterSpacing: 1 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 15, height: 54, borderRadius: 16, borderWidth: 1.5 },
|
||||
empresaNome: { fontSize: 17, fontWeight: '800', letterSpacing: -0.3 },
|
||||
tutorRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 4 },
|
||||
tutorText: { fontSize: 12, fontWeight: '600' },
|
||||
arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(26, 54, 93, 0.8)', justifyContent: 'flex-end' },
|
||||
modalContent: { borderTopLeftRadius: 40, borderTopRightRadius: 40, padding: 28, maxHeight: '90%' },
|
||||
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 30 },
|
||||
modalTitle: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
modalSub: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginTop: 4 },
|
||||
closeBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(0,0,0,0.05)', justifyContent: 'center', alignItems: 'center' },
|
||||
divider: { height: 1, marginVertical: 20, opacity: 0.5 },
|
||||
inputWrapper: { marginBottom: 20 },
|
||||
inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 58, borderRadius: 18, borderWidth: 1.5 },
|
||||
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
|
||||
saveBtn: { height: 58, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginTop: 20, elevation: 4 },
|
||||
saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1 }
|
||||
saveBtn: { height: 60, borderRadius: 22, justifyContent: 'center', alignItems: 'center', marginTop: 25, elevation: 6, shadowColor: '#2390a6', shadowOpacity: 0.3, shadowRadius: 10 },
|
||||
saveBtnText: { color: '#fff', fontSize: 16, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.2 }
|
||||
});
|
||||
|
||||
export default ListaEmpresasProfessor;
|
||||
@@ -37,7 +37,6 @@ export default function PerfilProfessor() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [perfil, setPerfil] = useState<PerfilData | null>(null);
|
||||
|
||||
// --- SISTEMA DE AVISOS MODERNOS ---
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
@@ -50,24 +49,25 @@ export default function PerfilProfessor() {
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, []);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
// Cores EPVC
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
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)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF',
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
vermelhoSuave: isDarkMode ? 'rgba(239, 68, 68, 0.15)' : '#FFF5F5',
|
||||
vermelho: '#EF4444',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
verde: '#38A169',
|
||||
}), [isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
carregarPerfil();
|
||||
}, []);
|
||||
useEffect(() => { carregarPerfil(); }, []);
|
||||
|
||||
async function carregarPerfil() {
|
||||
try {
|
||||
@@ -105,9 +105,9 @@ export default function PerfilProfessor() {
|
||||
|
||||
if (error) throw error;
|
||||
setEditando(false);
|
||||
showAlert('Perfil atualizado com sucesso!', 'success');
|
||||
showAlert('Perfil atualizado!', 'success');
|
||||
} catch (error: any) {
|
||||
showAlert('Erro ao gravar dados no servidor.', 'error');
|
||||
showAlert('Erro ao gravar dados.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,7 +128,6 @@ export default function PerfilProfessor() {
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
|
||||
{/* BANNER DE AVISO */}
|
||||
{alertConfig && (
|
||||
<Animated.View style={[
|
||||
styles.alertBar,
|
||||
@@ -145,118 +144,81 @@ export default function PerfilProfessor() {
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
||||
|
||||
{/* HEADER */}
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity style={[styles.backBtn, { backgroundColor: cores.card }]} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.texto} />
|
||||
<TouchableOpacity style={[styles.backBtn, { borderColor: cores.borda }]} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={22} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.topTitle, { color: cores.texto }]}>O Meu Perfil</Text>
|
||||
<Text style={[styles.topTitle, { color: cores.texto }]}>Perfil</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.editBtn, { backgroundColor: editando ? cores.azul : cores.card }]}
|
||||
style={[styles.editBtn, { backgroundColor: editando ? cores.laranja : cores.card, borderColor: editando ? cores.laranja : cores.borda }]}
|
||||
onPress={() => editando ? guardarPerfil() : setEditando(true)}
|
||||
>
|
||||
<Ionicons name={editando ? "checkmark" : "create-outline"} size={20} color={editando ? "#fff" : cores.azul} />
|
||||
<Ionicons name={editando ? "checkmark-sharp" : "create-outline"} size={20} color={editando ? "#fff" : cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* AVATAR SECTION */}
|
||||
<View style={styles.profileHeader}>
|
||||
<View style={[styles.avatarContainer, { borderColor: cores.azulSuave }]}>
|
||||
<View style={[styles.avatarBorder, { borderColor: cores.azul }]}>
|
||||
<View style={[styles.avatar, { backgroundColor: cores.azul }]}>
|
||||
<Text style={styles.avatarLetter}>{perfil?.nome?.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
{editando && (
|
||||
<View style={[styles.editBadge, { backgroundColor: cores.laranja }]}>
|
||||
<Ionicons name="camera" size={12} color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.userName, { color: cores.texto }]}>{perfil?.nome}</Text>
|
||||
<Text style={[styles.userRole, { color: cores.secundario }]}>
|
||||
{perfil?.curso || 'Professor / Tutor'}
|
||||
</Text>
|
||||
<View style={[styles.roleBadge, { backgroundColor: cores.azulSuave }]}>
|
||||
<Text style={[styles.userRole, { color: cores.azul }]}>{perfil?.curso || 'Professor'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* INFO CARD */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card }]}>
|
||||
<ModernInput
|
||||
label="Nome Completo"
|
||||
icon="person-outline"
|
||||
value={perfil?.nome || ''}
|
||||
editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)}
|
||||
cores={cores}
|
||||
/>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<ModernInput label="Nome Completo" icon="person" value={perfil?.nome || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, nome: v } : null)} cores={cores} />
|
||||
|
||||
<ModernInput
|
||||
label="Área / Curso"
|
||||
icon="book-outline"
|
||||
value={perfil?.curso || ''}
|
||||
editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)}
|
||||
cores={cores}
|
||||
/>
|
||||
<ModernInput label="Área de Formação" icon="school" value={perfil?.curso || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, curso: v } : null)} cores={cores} />
|
||||
|
||||
<ModernInput
|
||||
label="E-mail"
|
||||
icon="mail-outline"
|
||||
value={perfil?.email || ''}
|
||||
editable={false}
|
||||
cores={cores}
|
||||
/>
|
||||
<ModernInput label="Email Institucional" icon="mail" value={perfil?.email || ''} editable={false} cores={cores} />
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<ModernInput
|
||||
label="Nº Escola"
|
||||
icon="id-card-outline"
|
||||
value={perfil?.n_escola || ''}
|
||||
editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)}
|
||||
cores={cores}
|
||||
/>
|
||||
<ModernInput label="Nº Escola" icon="id-card" value={perfil?.n_escola || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, n_escola: v } : null)} cores={cores} />
|
||||
</View>
|
||||
<View style={{ flex: 1.5 }}>
|
||||
<ModernInput
|
||||
label="Telefone"
|
||||
icon="call-outline"
|
||||
value={perfil?.telefone || ''}
|
||||
editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)}
|
||||
keyboardType="phone-pad"
|
||||
cores={cores}
|
||||
/>
|
||||
<ModernInput label="Telemóvel" icon="call" value={perfil?.telefone || ''} editable={editando}
|
||||
onChangeText={(v: string) => setPerfil(prev => prev ? { ...prev, telefone: v } : null)} keyboardType="phone-pad" cores={cores} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ACTIONS */}
|
||||
<View style={styles.actionsContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.menuItem, { backgroundColor: cores.card }]}
|
||||
onPress={() => router.push('/Professor/redefenirsenha2')}
|
||||
>
|
||||
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push('/Professor/redefenirsenha2')}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={cores.azul} />
|
||||
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar Palavra-passe</Text>
|
||||
<Text style={[styles.menuText, { color: cores.texto }]}>Alterar palavra-passe</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.menuItem, { backgroundColor: cores.card }]}
|
||||
onPress={terminarSessao}
|
||||
>
|
||||
<TouchableOpacity style={[styles.menuItem, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={terminarSessao}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
|
||||
<Ionicons name="power" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: cores.vermelho }]}>Terminar Sessão</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{editando && (
|
||||
<TouchableOpacity
|
||||
style={[styles.cancelBtn]}
|
||||
onPress={() => { setEditando(false); carregarPerfil(); }}
|
||||
>
|
||||
<Text style={[styles.cancelText, { color: cores.secundario }]}>Cancelar Edição</Text>
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={() => { setEditando(false); carregarPerfil(); }}>
|
||||
<Text style={[styles.cancelText, { color: cores.laranja }]}>Cancelar Alterações</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@@ -269,21 +231,9 @@ export default function PerfilProfessor() {
|
||||
const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={[styles.inputLabel, { color: cores.secundario }]}>{label}</Text>
|
||||
<View style={[
|
||||
styles.inputContainer,
|
||||
{
|
||||
backgroundColor: cores.fundo,
|
||||
borderColor: editable ? cores.azul : cores.borda,
|
||||
opacity: editable ? 1 : 0.8
|
||||
}
|
||||
]}>
|
||||
<Ionicons name={icon} size={18} color={cores.azul} style={{ marginRight: 10 }} />
|
||||
<TextInput
|
||||
{...props}
|
||||
editable={editable}
|
||||
style={[styles.textInput, { color: cores.texto }]}
|
||||
placeholderTextColor={cores.secundario}
|
||||
/>
|
||||
<View style={[styles.inputContainer, { backgroundColor: editable ? '#fff' : cores.azulSuave, borderColor: editable ? cores.laranja : 'transparent' }]}>
|
||||
<Ionicons name={icon} size={18} color={editable ? cores.laranja : cores.azul} style={{ marginRight: 12 }} />
|
||||
<TextInput {...props} editable={editable} style={[styles.textInput, { color: cores.texto }]} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -291,54 +241,31 @@ const ModernInput = ({ label, icon, cores, editable, ...props }: any) => (
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
alertBar: {
|
||||
position: 'absolute',
|
||||
left: 20,
|
||||
right: 20,
|
||||
padding: 15,
|
||||
borderRadius: 15,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999,
|
||||
elevation: 10,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 5
|
||||
},
|
||||
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1 },
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15
|
||||
},
|
||||
backBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
editBtn: { width: 42, height: 42, borderRadius: 12, justifyContent: 'center', alignItems: 'center', elevation: 2 },
|
||||
topTitle: { fontSize: 18, fontWeight: '800' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
profileHeader: { alignItems: 'center', marginVertical: 30 },
|
||||
avatarContainer: { padding: 8, borderRadius: 100, borderWidth: 2, borderStyle: 'dashed' },
|
||||
avatar: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center', elevation: 5 },
|
||||
avatarLetter: { color: '#fff', fontSize: 32, fontWeight: '800' },
|
||||
userName: { fontSize: 22, fontWeight: '800', marginTop: 15 },
|
||||
userRole: { fontSize: 14, fontWeight: '500' },
|
||||
card: { borderRadius: 24, padding: 20, marginBottom: 20, elevation: 2, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 10 },
|
||||
inputWrapper: { marginBottom: 15 },
|
||||
inputLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginBottom: 6, marginLeft: 4 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, height: 52, borderRadius: 16, borderWidth: 1.5 },
|
||||
textInput: { flex: 1, fontSize: 15, fontWeight: '600' },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 },
|
||||
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, fontSize: 13 },
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
backBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
editBtn: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1, elevation: 2 },
|
||||
topTitle: { fontSize: 18, fontWeight: '900', letterSpacing: -0.5 },
|
||||
scrollContent: { paddingHorizontal: 24, paddingBottom: 50 },
|
||||
profileHeader: { alignItems: 'center', marginVertical: 35 },
|
||||
avatarBorder: { padding: 4, borderRadius: 100, borderWidth: 2, position: 'relative' },
|
||||
avatar: { width: 90, height: 90, borderRadius: 45, alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 10 },
|
||||
avatarLetter: { color: '#fff', fontSize: 36, fontWeight: '900' },
|
||||
editBadge: { position: 'absolute', bottom: 0, right: 0, width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 3, borderColor: '#fff' },
|
||||
userName: { fontSize: 24, fontWeight: '900', marginTop: 15, letterSpacing: -0.5 },
|
||||
roleBadge: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 },
|
||||
userRole: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
card: { borderRadius: 28, padding: 24, marginBottom: 20, borderWidth: 1 },
|
||||
inputWrapper: { marginBottom: 18 },
|
||||
inputLabel: { fontSize: 10, fontWeight: '900', textTransform: 'uppercase', marginBottom: 8, marginLeft: 4, letterSpacing: 0.5 },
|
||||
inputContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, height: 56, borderRadius: 18, borderWidth: 1.5 },
|
||||
textInput: { flex: 1, fontSize: 15, fontWeight: '700' },
|
||||
row: { flexDirection: 'row' },
|
||||
actionsContainer: { gap: 12 },
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 14,
|
||||
borderRadius: 20,
|
||||
elevation: 1
|
||||
},
|
||||
menuIcon: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
menuText: { flex: 1, marginLeft: 12, fontSize: 15, fontWeight: '700' },
|
||||
menuItem: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 22, borderWidth: 1 },
|
||||
menuIcon: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
menuText: { flex: 1, marginLeft: 15, fontSize: 15, fontWeight: '800' },
|
||||
cancelBtn: { marginTop: 25, alignItems: 'center' },
|
||||
cancelText: { fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' }
|
||||
cancelText: { fontSize: 14, fontWeight: '800', textDecorationLine: 'underline' }
|
||||
});
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
// Importação correta para controlo total de áreas seguras
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
@@ -22,19 +21,22 @@ const { width } = Dimensions.get('window');
|
||||
export default function ProfessorMenu() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const insets = useSafeAreaInsets(); // Para ajustes finos se necessário
|
||||
const insets = useSafeAreaInsets();
|
||||
const [nome, setNome] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
// Paleta EPVC Extraída da Imagem
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Fundo branco limpo como o login
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D', // Azul escuro para contraste
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}), [isDarkMode]);
|
||||
|
||||
@@ -61,64 +63,63 @@ export default function ProfessorMenu() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// Aplicamos as edges para garantir que o fundo cobre tudo mas o conteúdo respeita os limites
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.content,
|
||||
{ paddingBottom: insets.bottom + 20 } // Garante que o último card não cole no fundo
|
||||
{ paddingBottom: insets.bottom + 20 }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{/* HEADER */}
|
||||
{/* HEADER EPVC STYLE */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerRow}>
|
||||
<View>
|
||||
<Text style={[styles.welcome, { color: cores.textoSecundario }]}>Bem-vindo,</Text>
|
||||
<Text style={[styles.welcome, { color: cores.laranja }]}>Olá, Professor</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={cores.azul} style={{ marginTop: 8, alignSelf: 'flex-start' }} />
|
||||
) : (
|
||||
<Text style={[styles.name, { color: cores.texto }]}>{nome || 'Professor'}</Text>
|
||||
<Text style={[styles.name, { color: cores.texto }]}>{nome || 'Docente'}</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/Professor/PerfilProf')}
|
||||
style={[styles.avatarMini, { backgroundColor: cores.azul }]}
|
||||
>
|
||||
<Text style={styles.avatarTxt}>{nome?.charAt(0).toUpperCase()}</Text>
|
||||
<Text style={styles.avatarTxt}>{nome?.charAt(0).toUpperCase() || 'P'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.infoBanner, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<Ionicons name="stats-chart" size={18} color={cores.azul} />
|
||||
<Text style={[styles.bannerTxt, { color: cores.textoSecundario }]}>Painel de Gestão de Estágios</Text>
|
||||
<View style={[styles.infoBanner, { backgroundColor: cores.azulSuave, borderColor: cores.azul }]}>
|
||||
<Ionicons name="school-outline" size={18} color={cores.azul} />
|
||||
<Text style={[styles.bannerTxt, { color: cores.azul }]}>Escola Profissional de Vila do Conde</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* GRID */}
|
||||
{/* GRID DE MENU */}
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="document-text" title="Sumários" subtitle="Registos Diários" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} />
|
||||
<MenuCard icon="calendar" title="Presenças" subtitle="Controlo de Fluxo" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} />
|
||||
<MenuCard icon="alert-circle" title="Faltas" subtitle="Justificações" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} />
|
||||
<MenuCard icon="people" title="Alunos" subtitle="Gestão de Turmas" onPress={() => router.push('/Professor/Alunos/ListaAlunos')} cores={cores} />
|
||||
<MenuCard icon="briefcase" title="Estágios" subtitle="Novos Projetos" onPress={() => router.push('/Professor/Alunos/Estagios')} cores={cores} />
|
||||
<MenuCard icon="business" title="Empresas" subtitle="Parcerias Ativas" onPress={() => router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} />
|
||||
<MenuCard icon="settings" title="Definições" subtitle="Sistema" onPress={() => router.push('/Professor/defenicoes2')} cores={cores} />
|
||||
<MenuCard icon="person" title="Perfil" subtitle="Minha Conta" onPress={() => router.push('/Professor/PerfilProf')} cores={cores} />
|
||||
<MenuCard icon="document-text-outline" title="Sumários" subtitle="Registos" onPress={() => router.push('/Professor/Alunos/Sumarios')} cores={cores} />
|
||||
<MenuCard icon="calendar-outline" title="Presenças" subtitle="Controlo" onPress={() => router.push('/Professor/Alunos/Presencas')} cores={cores} />
|
||||
<MenuCard icon="alert-circle-outline" title="Faltas" subtitle="Gestão" onPress={() => router.push('/Professor/Alunos/Faltas')} cores={cores} />
|
||||
<MenuCard icon="people-outline" title="Alunos" subtitle="Turmas" onPress={() => router.push('/Professor/Alunos/ListaAlunos')} cores={cores} />
|
||||
<MenuCard icon="briefcase-outline" title="Estágios" subtitle="Projetos" onPress={() => router.push('/Professor/Alunos/Estagios')} cores={cores} />
|
||||
<MenuCard icon="business-outline" title="Empresas" subtitle="Parcerias" onPress={() => router.push('/Professor/Empresas/ListaEmpresas')} cores={cores} />
|
||||
<MenuCard icon="settings-outline" title="Definições" subtitle="Sistema" onPress={() => router.push('/Professor/defenicoes2')} cores={cores} />
|
||||
<MenuCard icon="person-outline" title="Perfil" subtitle="A minha conta" onPress={() => router.push('/Professor/PerfilProf')} cores={cores} />
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>EPVC • Gestão de Estágios</Text>
|
||||
<View style={[styles.footerLine, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>Estágios+ • EPVC 2026</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// O MenuCard permanece igual, apenas garantindo consistência
|
||||
function MenuCard({ icon, title, subtitle, onPress, cores }: any) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -127,45 +128,46 @@ function MenuCard({ icon, title, subtitle, onPress, cores }: any) {
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name={icon} size={22} color={cores.azul} />
|
||||
<Ionicons name={icon} size={24} color={cores.azul} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.cardTitle, { color: cores.texto }]}>{title}</Text>
|
||||
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]}>{subtitle}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.textoSecundario} style={styles.arrow} />
|
||||
<View style={[styles.arrowCircle, { backgroundColor: cores.laranja }]}>
|
||||
<Ionicons name="chevron-forward" size={12} color="#FFF" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: { padding: 20 },
|
||||
header: { marginBottom: 30 },
|
||||
content: { padding: 24 },
|
||||
header: { marginBottom: 35 },
|
||||
headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
welcome: { fontSize: 13, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
name: { fontSize: 24, fontWeight: '900', letterSpacing: -0.5, marginTop: 4 },
|
||||
avatarMini: { width: 48, height: 48, borderRadius: 16, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
||||
avatarTxt: { color: '#fff', fontSize: 20, fontWeight: '800' },
|
||||
infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 16, borderWidth: 1 },
|
||||
bannerTxt: { fontSize: 12, fontWeight: '700', marginLeft: 10, letterSpacing: 0.3 },
|
||||
welcome: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 },
|
||||
name: { fontSize: 26, fontWeight: '900', letterSpacing: -0.8, marginTop: 2 },
|
||||
avatarMini: { width: 52, height: 52, borderRadius: 18, justifyContent: 'center', alignItems: 'center', elevation: 4, shadowColor: '#000', shadowOpacity: 0.15, shadowRadius: 8 },
|
||||
avatarTxt: { color: '#fff', fontSize: 22, fontWeight: '900' },
|
||||
infoBanner: { flexDirection: 'row', alignItems: 'center', padding: 14, borderRadius: 16, borderWidth: 1, borderStyle: 'dashed' },
|
||||
bannerTxt: { fontSize: 13, fontWeight: '700', marginLeft: 10 },
|
||||
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
|
||||
card: {
|
||||
width: (width - 55) / 2, // Ajuste para melhor margem entre cards
|
||||
borderRadius: 28,
|
||||
padding: 18,
|
||||
marginBottom: 15,
|
||||
width: (width - 64) / 2,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
shadowOpacity: 0.03,
|
||||
shadowRadius: 15,
|
||||
elevation: 3,
|
||||
},
|
||||
iconBox: { width: 40, height: 40, borderRadius: 12, justifyContent: 'center', alignItems: 'center', marginBottom: 15 },
|
||||
cardTitle: { fontSize: 15, fontWeight: '800' },
|
||||
cardSubtitle: { fontSize: 11, marginTop: 2, fontWeight: '600', opacity: 0.8 },
|
||||
arrow: { position: 'absolute', top: 18, right: 15, opacity: 0.3 },
|
||||
footer: { marginTop: 20, alignItems: 'center', opacity: 0.5 },
|
||||
footerTxt: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 }
|
||||
iconBox: { width: 48, height: 48, borderRadius: 15, justifyContent: 'center', alignItems: 'center', marginBottom: 16 },
|
||||
cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.2 },
|
||||
cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600', opacity: 0.7 },
|
||||
arrowCircle: { position: 'absolute', top: 20, right: 15, width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center' },
|
||||
footer: { marginTop: 30, alignItems: 'center' },
|
||||
footerLine: { width: 40, height: 4, borderRadius: 2, marginBottom: 15 },
|
||||
footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 }
|
||||
});
|
||||
@@ -23,7 +23,6 @@ const Definicoes = memo(() => {
|
||||
const { isDarkMode, toggleTheme } = useTheme();
|
||||
const [notificacoes, setNotificacoes] = useState(true);
|
||||
|
||||
// --- SISTEMA DE AVISOS MODERNOS ---
|
||||
const [alertConfig, setAlertConfig] = useState<{ msg: string, type: 'success' | 'error' | 'info' } | null>(null);
|
||||
const alertOpacity = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
@@ -36,19 +35,22 @@ const Definicoes = memo(() => {
|
||||
]).start(() => setAlertConfig(null));
|
||||
}, []);
|
||||
|
||||
const azulPetroleo = '#2390a6';
|
||||
// Cores EPVC
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F1F5F9',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulPetroleo,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : 'rgba(35, 144, 166, 0.08)',
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#FFFFFF', // Branco puro do login
|
||||
card: isDarkMode ? '#161618' : '#F8FAFC',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1A365D',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#718096',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#F0F9FA',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: 'rgba(239, 68, 68, 0.1)',
|
||||
vermelhoSuave: '#FFF5F5',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
verde: '#38A169',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -71,7 +73,6 @@ const Definicoes = memo(() => {
|
||||
<View style={{ flex: 1, backgroundColor: cores.fundo }}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
{/* BANNER DE FEEDBACK */}
|
||||
{alertConfig && (
|
||||
<Animated.View style={[
|
||||
styles.alertBar,
|
||||
@@ -88,58 +89,58 @@ const Definicoes = memo(() => {
|
||||
|
||||
<SafeAreaView style={styles.safe} edges={['top']}>
|
||||
|
||||
{/* HEADER ESTILIZADO */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnVoltar, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
style={[styles.btnVoltar, { borderColor: cores.borda }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
<Ionicons name="chevron-back" size={24} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<Text style={[styles.tituloGeral, { color: cores.texto }]}>Definições</Text>
|
||||
<Text style={[styles.tituloSub, { color: cores.laranja }]}>Sistema & Suporte</Text>
|
||||
</View>
|
||||
<View style={{ width: 45 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* GRUPO: PERSONALIZAÇÃO */}
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Personalização</Text>
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario }]}>Preferências</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.item, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="notifications" size={20} color={cores.azul} />
|
||||
<Ionicons name="notifications-outline" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações Push</Text>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Notificações</Text>
|
||||
<Switch
|
||||
value={notificacoes}
|
||||
onValueChange={(v) => {
|
||||
setNotificacoes(v);
|
||||
showAlert(v ? "Notificações ativadas" : "Notificações desativadas", "info");
|
||||
showAlert(v ? "Notificações ligadas" : "Notificações desligadas", "info");
|
||||
}}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.azul }}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.laranja }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.item}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: isDarkMode ? '#334155' : '#F1F5F9' }]}>
|
||||
<Ionicons name={isDarkMode ? "moon" : "sunny"} size={20} color={isDarkMode ? '#FACC15' : '#F59E0B'} />
|
||||
<Ionicons name={isDarkMode ? "moon-outline" : "sunny-outline"} size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Interface Escura</Text>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Modo Escuro</Text>
|
||||
<Switch
|
||||
value={isDarkMode}
|
||||
onValueChange={toggleTheme}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.azul }}
|
||||
trackColor={{ false: '#CBD5E1', true: cores.laranja }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* GRUPO: CONTACTOS EPVC */}
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 25 }]}>Escola Profissional</Text>
|
||||
<Text style={[styles.sectionLabel, { color: cores.secundario, marginTop: 30 }]}>Escola Profissional</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<SettingLink
|
||||
icon="business"
|
||||
icon="at-outline"
|
||||
label="Direção Geral"
|
||||
subLabel="epvc@epvc.pt"
|
||||
onPress={() => abrirURL('mailto:epvc@epvc.pt')}
|
||||
@@ -147,15 +148,15 @@ const Definicoes = memo(() => {
|
||||
showBorder
|
||||
/>
|
||||
<SettingLink
|
||||
icon="mail"
|
||||
icon="mail-unread-outline"
|
||||
label="Secretaria"
|
||||
subLabel="secretaria@epvc.pt"
|
||||
subLabel="Atendimento ao Docente"
|
||||
onPress={() => abrirURL('mailto:secretaria@epvc.pt')}
|
||||
cores={cores}
|
||||
showBorder
|
||||
/>
|
||||
<SettingLink
|
||||
icon="call"
|
||||
icon="call-outline"
|
||||
label="Linha Direta"
|
||||
subLabel="252 641 805"
|
||||
onPress={() => abrirURL('tel:252641805')}
|
||||
@@ -163,28 +164,28 @@ const Definicoes = memo(() => {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* GRUPO: SEGURANÇA & INFO */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 25 }]}>
|
||||
<View style={[styles.item, { borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, marginTop: 30 }]}>
|
||||
<View style={[styles.item, { borderBottomWidth: 1, borderBottomColor: cores.borda }]}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="shield-checkmark" size={20} color={cores.azul} />
|
||||
<Ionicons name="code-working-outline" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>Versão Estável</Text>
|
||||
<Text style={[styles.versionBadge, { color: cores.secundario }]}>v2.6.17</Text>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto }]}>App Version</Text>
|
||||
<Text style={[styles.versionBadge, { color: cores.azul, backgroundColor: cores.azulSuave }]}>2.6.17</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.item} onPress={handleLogout}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: cores.vermelhoSuave }]}>
|
||||
<Ionicons name="log-out" size={20} color={cores.vermelho} />
|
||||
<Ionicons name="log-out-outline" size={20} color={cores.vermelho} />
|
||||
</View>
|
||||
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '800' }]}>Terminar Sessão</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.vermelho} opacity={0.5} />
|
||||
<Text style={[styles.itemTexto, { color: cores.vermelho, fontWeight: '900' }]}>Terminar Sessão</Text>
|
||||
<Ionicons name="arrow-forward-circle-outline" size={20} color={cores.vermelho} opacity={0.5} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={[styles.footerText, { color: cores.secundario }]}>Desenvolvido para PAP • 2026</Text>
|
||||
<Text style={[styles.footerText, { color: cores.azul, fontWeight: '800', marginTop: 4 }]}>EPVC</Text>
|
||||
<View style={[styles.footerLine, { backgroundColor: cores.laranja }]} />
|
||||
<Text style={[styles.footerText, { color: cores.secundario }]}>Estágios+ • Vila do Conde</Text>
|
||||
<Text style={[styles.footerText, { color: cores.azul, marginTop: 4, fontSize: 13 }]}>EPVC</Text>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
@@ -193,7 +194,6 @@ const Definicoes = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// Componente Auxiliar para Links
|
||||
const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.item, showBorder && { borderBottomWidth: 1, borderBottomColor: cores.borda }]}
|
||||
@@ -204,28 +204,33 @@ const SettingLink = ({ icon, label, subLabel, onPress, cores, showBorder }: any)
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 15 }}>
|
||||
<Text style={[styles.itemTexto, { color: cores.texto, marginLeft: 0 }]}>{label}</Text>
|
||||
<Text style={{ color: cores.secundario, fontSize: 12, marginTop: 2 }}>{subLabel}</Text>
|
||||
<Text style={{ color: cores.secundario, fontSize: 12, fontWeight: '600' }}>{subLabel}</Text>
|
||||
</View>
|
||||
<View style={[styles.arrowCircle, { borderColor: cores.borda }]}>
|
||||
<Ionicons name="chevron-forward" size={14} color={cores.azul} />
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={cores.secundario} opacity={0.3} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 16, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 8 },
|
||||
alertText: { color: '#fff', fontWeight: '700', marginLeft: 10, flex: 1, fontSize: 14 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 10 },
|
||||
btnVoltar: { width: 45, height: 45, borderRadius: 15, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
alertBar: { position: 'absolute', left: 20, right: 20, padding: 15, borderRadius: 12, flexDirection: 'row', alignItems: 'center', zIndex: 9999, elevation: 5 },
|
||||
alertText: { color: '#fff', fontWeight: '800', marginLeft: 10, flex: 1, fontSize: 13 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 15 },
|
||||
btnVoltar: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 1 },
|
||||
tituloGeral: { fontSize: 22, fontWeight: '900', letterSpacing: -0.5 },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 50 },
|
||||
sectionLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', marginBottom: 12, marginLeft: 8, letterSpacing: 1.2 },
|
||||
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.03, shadowRadius: 15, elevation: 2 },
|
||||
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16 },
|
||||
tituloSub: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
scrollContent: { paddingHorizontal: 24, paddingBottom: 60 },
|
||||
sectionLabel: { fontSize: 11, fontWeight: '900', textTransform: 'uppercase', marginBottom: 15, marginLeft: 4, letterSpacing: 1 },
|
||||
card: { borderRadius: 28, paddingHorizontal: 20, borderWidth: 1, shadowColor: '#000', shadowOpacity: 0.02, shadowRadius: 10, elevation: 2 },
|
||||
item: { flexDirection: 'row', alignItems: 'center', paddingVertical: 18 },
|
||||
iconContainer: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700' },
|
||||
versionBadge: { fontSize: 13, fontWeight: 'bold', backgroundColor: 'rgba(0,0,0,0.05)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
footer: { alignItems: 'center', marginTop: 40, opacity: 0.6 },
|
||||
footerText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1 }
|
||||
itemTexto: { flex: 1, marginLeft: 15, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 },
|
||||
versionBadge: { fontSize: 11, fontWeight: '900', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
||||
arrowCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
footer: { alignItems: 'center', marginTop: 50 },
|
||||
footerLine: { width: 30, height: 3, borderRadius: 2, marginBottom: 15 },
|
||||
footerText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.5 }
|
||||
});
|
||||
|
||||
export default Definicoes;
|
||||
Reference in New Issue
Block a user