Versão 26.01.10 - Atualizações
This commit is contained in:
294
app/AlunoHome.tsx
Normal file
294
app/AlunoHome.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import { useRouter } from 'expo-router';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View
|
||||
} from 'react-native';
|
||||
import { Calendar, LocaleConfig } from 'react-native-calendars';
|
||||
import { useTheme } from '../themecontext';
|
||||
|
||||
// Configuração PT
|
||||
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'],
|
||||
dayNames: ['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'],
|
||||
dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
|
||||
};
|
||||
LocaleConfig.defaultLocale = 'pt';
|
||||
|
||||
// --- FUNÇÃO PARA CALCULAR FERIADOS (Nacionais + Vila do Conde) ---
|
||||
const getFeriadosMap = (ano: number) => {
|
||||
const f: Record<string, string> = {
|
||||
[`${ano}-01-01`]: "Ano Novo",
|
||||
[`${ano}-04-25`]: "Dia da Liberdade",
|
||||
[`${ano}-05-01`]: "Dia do Trabalhador",
|
||||
[`${ano}-06-10`]: "Dia de Portugal",
|
||||
[`${ano}-06-24`]: "São João (Vila do Conde)", // Feriado Municipal
|
||||
[`${ano}-08-15`]: "Assunção de Nª Senhora",
|
||||
[`${ano}-10-05`]: "Implantação da República",
|
||||
[`${ano}-11-01`]: "Todos os Santos",
|
||||
[`${ano}-12-01`]: "Restauração da Independência",
|
||||
[`${ano}-12-08`]: "Imaculada Conceição",
|
||||
[`${ano}-12-25`]: "Natal"
|
||||
};
|
||||
|
||||
const a = ano % 19, b = Math.floor(ano / 100), c = ano % 100;
|
||||
const d = Math.floor(b / 4), e = b % 4, f_calc = Math.floor((b + 8) / 25);
|
||||
const g = Math.floor((b - f_calc + 1) / 3), h = (19 * a + b - d - g + 15) % 30;
|
||||
const i = Math.floor(c / 4), k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
const mes = Math.floor((h + l - 7 * m + 114) / 31);
|
||||
const dia = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
|
||||
const pascoa = new Date(ano, mes - 1, dia);
|
||||
const formatar = (dt: Date) => dt.toISOString().split('T')[0];
|
||||
|
||||
f[formatar(pascoa)] = "Páscoa";
|
||||
f[formatar(new Date(pascoa.getTime() - 47 * 24 * 3600 * 1000))] = "Carnaval";
|
||||
f[formatar(new Date(pascoa.getTime() - 2 * 24 * 3600 * 1000))] = "Sexta-feira Santa";
|
||||
f[formatar(new Date(pascoa.getTime() + 60 * 24 * 3600 * 1000))] = "Corpo de Deus";
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
const AlunoHome = memo(() => {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const hojeStr = new Date().toISOString().split('T')[0];
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState(hojeStr);
|
||||
const [configEstagio, setConfigEstagio] = useState({ inicio: '2026-01-05', fim: '2026-05-30' });
|
||||
const [presencas, setPresencas] = useState<Record<string, boolean>>({});
|
||||
const [faltas, setFaltas] = useState<Record<string, boolean>>({});
|
||||
const [sumarios, setSumarios] = useState<Record<string, string>>({});
|
||||
const [faltasJustificadas, setFaltasJustificadas] = useState<Record<string, any>>({});
|
||||
const [pdf, setPdf] = useState<any>(null);
|
||||
const [editandoSumario, setEditandoSumario] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const carregarTudo = async () => {
|
||||
try {
|
||||
const [config, pres, falt, sums, just] = await Promise.all([
|
||||
AsyncStorage.getItem('@dados_estagio'),
|
||||
AsyncStorage.getItem('@presencas'),
|
||||
AsyncStorage.getItem('@faltas'),
|
||||
AsyncStorage.getItem('@sumarios'),
|
||||
AsyncStorage.getItem('@justificacoes')
|
||||
]);
|
||||
if (config) setConfigEstagio(JSON.parse(config));
|
||||
if (pres) setPresencas(JSON.parse(pres));
|
||||
if (falt) setFaltas(JSON.parse(falt));
|
||||
if (sums) setSumarios(JSON.parse(sums));
|
||||
if (just) setFaltasJustificadas(JSON.parse(just));
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
carregarTudo();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const themeStyles = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#fff' : '#000',
|
||||
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
|
||||
borda: isDarkMode ? '#333' : '#ddd',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
|
||||
const listaFeriados = useMemo(() => Object.keys(feriadosMap), [feriadosMap]);
|
||||
|
||||
const infoData = useMemo(() => {
|
||||
const data = new Date(selectedDate);
|
||||
const diaSemana = data.getDay();
|
||||
const ehFimDeSemana = diaSemana === 0 || diaSemana === 6;
|
||||
const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
|
||||
const ehFuturo = selectedDate > hojeStr;
|
||||
const nomeFeriado = feriadosMap[selectedDate];
|
||||
|
||||
return {
|
||||
valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado,
|
||||
podeMarcarPresenca: !ehFimDeSemana && !foraDoIntervalo && !ehFuturo && !nomeFeriado,
|
||||
ehFuturo,
|
||||
nomeFeriado
|
||||
};
|
||||
}, [selectedDate, configEstagio, hojeStr, feriadosMap]);
|
||||
|
||||
const diasMarcados: any = useMemo(() => {
|
||||
const marcacoes: any = {};
|
||||
listaFeriados.forEach(d => { marcacoes[d] = { marked: true, dotColor: '#0dcaf0' }; });
|
||||
Object.keys(presencas).forEach((d) => {
|
||||
const temSumario = sumarios[d] && sumarios[d].trim().length > 0;
|
||||
marcacoes[d] = { marked: true, dotColor: temSumario ? '#198754' : '#ffc107' };
|
||||
});
|
||||
Object.keys(faltas).forEach((d) => {
|
||||
marcacoes[d] = { marked: true, dotColor: faltasJustificadas[d] ? '#6c757d' : '#dc3545' };
|
||||
});
|
||||
marcacoes[selectedDate] = { ...(marcacoes[selectedDate] || {}), selected: true, selectedColor: '#0d6efd' };
|
||||
return marcacoes;
|
||||
}, [presencas, faltas, sumarios, faltasJustificadas, selectedDate, listaFeriados]);
|
||||
|
||||
const handlePresenca = async () => {
|
||||
if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "Data inválida.");
|
||||
const novas = { ...presencas, [selectedDate]: true };
|
||||
setPresencas(novas);
|
||||
await AsyncStorage.setItem('@presencas', JSON.stringify(novas));
|
||||
};
|
||||
|
||||
const handleFalta = async () => {
|
||||
if (!infoData.valida) return Alert.alert("Bloqueado", "Data inválida.");
|
||||
const novas = { ...faltas, [selectedDate]: true };
|
||||
setFaltas(novas);
|
||||
await AsyncStorage.setItem('@faltas', JSON.stringify(novas));
|
||||
};
|
||||
|
||||
const guardarSumario = async () => {
|
||||
await AsyncStorage.setItem('@sumarios', JSON.stringify(sumarios));
|
||||
setEditandoSumario(false);
|
||||
Alert.alert("Sucesso", "Sumário guardado!");
|
||||
};
|
||||
|
||||
const escolherPDF = async () => {
|
||||
const result = await DocumentPicker.getDocumentAsync({ type: 'application/pdf' });
|
||||
if (!result.canceled) setPdf(result.assets[0]);
|
||||
};
|
||||
|
||||
const enviarJustificacao = async () => {
|
||||
if (!pdf) return Alert.alert('Aviso', 'Anexe um PDF.');
|
||||
const novas = { ...faltasJustificadas, [selectedDate]: pdf };
|
||||
setFaltasJustificadas(novas);
|
||||
await AsyncStorage.setItem('@justificacoes', JSON.stringify(novas));
|
||||
setPdf(null);
|
||||
};
|
||||
|
||||
const visualizarDocumento = async (uri: string) => {
|
||||
if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(uri);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.push('/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('/definicoes')}>
|
||||
<Ionicons name="settings-outline" size={26} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.botoesLinha}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, styles.btnPresenca, (!infoData.podeMarcarPresenca || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
onPress={handlePresenca}
|
||||
disabled={!infoData.podeMarcarPresenca || !!presencas[selectedDate] || !!faltas[selectedDate]}
|
||||
>
|
||||
<Text style={styles.txtBtn}>Marcar Presença</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, styles.btnFalta, (!infoData.valida || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
onPress={handleFalta}
|
||||
disabled={!infoData.valida || !!presencas[selectedDate] || !!faltas[selectedDate]}
|
||||
>
|
||||
<Text style={styles.txtBtn}>{infoData.ehFuturo ? "Vou Faltar" : "Faltei"}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.cardCalendar, { backgroundColor: themeStyles.card }]}>
|
||||
<Calendar
|
||||
theme={{ calendarBackground: themeStyles.card, dayTextColor: themeStyles.texto, monthTextColor: themeStyles.texto, todayTextColor: '#0d6efd', arrowColor: '#0d6efd' }}
|
||||
markedDates={diasMarcados}
|
||||
onDayPress={(day) => { setSelectedDate(day.dateString); setEditandoSumario(false); }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{infoData.nomeFeriado && (
|
||||
<View style={styles.cardFeriado}>
|
||||
<Ionicons name="gift-outline" size={18} color="#0dcaf0" />
|
||||
<Text style={styles.txtFeriado}>
|
||||
{selectedDate.endsWith('-06-24') ? "É Feriado em Vila do Conde: São João" : `Feriado: ${infoData.nomeFeriado}`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{presencas[selectedDate] && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
|
||||
<View style={styles.rowTitle}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto }]}>Sumário do Dia</Text>
|
||||
{!editandoSumario && (
|
||||
<TouchableOpacity onPress={() => setEditandoSumario(true)}>
|
||||
<Ionicons name="create-outline" size={22} color="#0d6efd" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { borderColor: themeStyles.borda, color: themeStyles.texto, opacity: editandoSumario ? 1 : 0.7 }]}
|
||||
multiline editable={editandoSumario}
|
||||
value={sumarios[selectedDate] || ''}
|
||||
onChangeText={(txt) => setSumarios({ ...sumarios, [selectedDate]: txt })}
|
||||
placeholder="Descreve as tuas tarefas..."
|
||||
placeholderTextColor={themeStyles.textoSecundario}
|
||||
/>
|
||||
{editandoSumario && (
|
||||
<TouchableOpacity style={styles.btnGuardar} onPress={guardarSumario}>
|
||||
<Text style={styles.txtBtn}>Guardar Sumário</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{faltas[selectedDate] && (
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
|
||||
<Text style={[styles.cardTitulo, { color: themeStyles.texto, marginBottom: 15 }]}>Justificação de Falta</Text>
|
||||
{faltasJustificadas[selectedDate] ? (
|
||||
<TouchableOpacity style={styles.btnVer} onPress={() => visualizarDocumento(faltasJustificadas[selectedDate].uri)}>
|
||||
<Ionicons name="document-text-outline" size={20} color="#fff" />
|
||||
<Text style={[styles.txtBtn, { marginLeft: 8 }]}>Ver Justificação (PDF)</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View>
|
||||
<TouchableOpacity style={[styles.btnAnexar, { borderColor: themeStyles.borda }]} onPress={escolherPDF}>
|
||||
<Text style={{ color: themeStyles.texto }}>{pdf ? pdf.name : 'Anexar PDF'}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btnGuardar, !pdf && styles.disabled]} onPress={enviarJustificacao} disabled={!pdf}>
|
||||
<Text style={styles.txtBtn}>Enviar Justificação</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
container: { padding: 20 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
||||
title: { fontSize: 20, fontWeight: 'bold' },
|
||||
botoesLinha: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
|
||||
btn: { padding: 15, borderRadius: 12, width: '48%', alignItems: 'center' },
|
||||
btnPresenca: { backgroundColor: '#0d6efd' },
|
||||
btnFalta: { backgroundColor: '#dc3545' },
|
||||
btnGuardar: { backgroundColor: '#198754', padding: 12, borderRadius: 10, marginTop: 10, alignItems: 'center' },
|
||||
btnAnexar: { borderWidth: 1, padding: 12, borderRadius: 10, marginBottom: 10, alignItems: 'center', borderStyle: 'dashed' },
|
||||
btnVer: { backgroundColor: '#6c757d', padding: 12, borderRadius: 10, flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
|
||||
disabled: { opacity: 0.3 },
|
||||
txtBtn: { color: '#fff', fontWeight: 'bold' },
|
||||
cardCalendar: { borderRadius: 16, elevation: 4, padding: 10, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 10 },
|
||||
cardFeriado: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 15, backgroundColor: 'rgba(13, 202, 240, 0.1)', padding: 10, borderRadius: 10 },
|
||||
txtFeriado: { color: '#0dcaf0', fontWeight: 'bold', marginLeft: 8 },
|
||||
card: { padding: 16, borderRadius: 16, marginTop: 20, elevation: 2 },
|
||||
cardTitulo: { fontSize: 16, fontWeight: 'bold' },
|
||||
rowTitle: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 },
|
||||
input: { borderWidth: 1, borderRadius: 10, padding: 12, height: 100, textAlignVertical: 'top' }
|
||||
});
|
||||
|
||||
export default AlunoHome;
|
||||
25
app/_layout.tsx
Normal file
25
app/_layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
// app/_layout.tsx
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { ThemeProvider, useTheme } from '../themecontext';
|
||||
|
||||
function RootLayoutContent() {
|
||||
const { isDarkMode } = useTheme();
|
||||
return (
|
||||
<>
|
||||
<StatusBar style={isDarkMode ? "light" : "dark"} />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
{/* Removido o .tsx do name, o Expo Router usa apenas o nome do ficheiro */}
|
||||
<Stack.Screen name="index" />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<RootLayoutContent />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
161
app/definicoes.tsx
Normal file
161
app/definicoes.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { memo, useMemo, useState } from 'react'; // Importado useMemo e memo
|
||||
import {
|
||||
Alert,
|
||||
Linking,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../themecontext';
|
||||
|
||||
const Definicoes = memo(() => {
|
||||
const router = useRouter();
|
||||
const [notificacoes, setNotificacoes] = useState(true);
|
||||
const { isDarkMode, toggleTheme } = useTheme();
|
||||
|
||||
// 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'
|
||||
}), [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 abrirEmail = () => Linking.openURL(`mailto:epvc@epvc.pt`);
|
||||
const abrirEmail2 = () => Linking.openURL(`mailto:secretaria@epvc.pt`);
|
||||
const abrirTelefone = () => Linking.openURL('tel:252 641 805');
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { 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>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.linha, { borderBottomWidth: 0 }]}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
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' }
|
||||
});
|
||||
|
||||
export default Definicoes;
|
||||
145
app/index.tsx
Normal file
145
app/index.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
|
||||
// app/index.tsx - TELA DE LOGIN
|
||||
|
||||
|
||||
import { Link, useRouter } from 'expo-router';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const router = useRouter(); // Inicializa o router
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Atenção', 'Por favor, preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
Alert.alert('Email inválido', 'Por favor, insira um email válido');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// SIMULAÇÃO DE LOGIN
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
|
||||
// Primeiro navega para a dashboard
|
||||
router.replace('/AlunoHome'); // ⬅️ Certifica-te que o ficheiro é app/dashboard.tsx
|
||||
|
||||
// Depois mostra alert de boas-vindas (opcional)
|
||||
setTimeout(() => {
|
||||
Alert.alert('Login realizado!', `Bem-vindo(a), ${email.split('@')[0]}!`);
|
||||
}, 300); // delay pequeno para garantir que a navegação ocorreu
|
||||
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
|
||||
{/* LOGO/TÍTULO */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>📱 Estágios+</Text>
|
||||
<Text style={styles.subtitle}>Escola Profissional de Vila do Conde</Text>
|
||||
</View>
|
||||
|
||||
{/* FORMULÁRIO */}
|
||||
<View style={styles.form}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu email"
|
||||
placeholderTextColor="#999"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Palavra-passe</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira a sua palavra-passe"
|
||||
placeholderTextColor="#999"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{/* BOTÃO ENTRAR */}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>ENTRAR</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* LINK ESQUECI SENHA */}
|
||||
<TouchableOpacity style={styles.forgotLink}>
|
||||
<Text style={styles.forgotText}>Esqueceu-se da palavra-passe?</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* CADASTRO */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Não tem uma conta?</Text>
|
||||
<Link href="/register" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={styles.registerText}> Crie uma conta agora</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
// ESTILOS
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#f8f9fa' },
|
||||
content: { flex: 1, justifyContent: 'center', paddingHorizontal: 24 },
|
||||
header: { alignItems: 'center', marginBottom: 48 },
|
||||
title: { fontSize: 32, fontWeight: '800', color: '#2d3436', marginBottom: 8 },
|
||||
subtitle: { fontSize: 16, color: '#636e72', textAlign: 'center' },
|
||||
form: { backgroundColor: '#fff', borderRadius: 20, padding: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 5 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: '#2d3436', marginBottom: 8, marginLeft: 4 },
|
||||
input: { backgroundColor: '#f8f9fa', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, marginBottom: 20, borderWidth: 1, borderColor: '#dfe6e9', color: '#2d3436' },
|
||||
button: { backgroundColor: '#0984e3', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 8, marginBottom: 24 },
|
||||
buttonDisabled: { backgroundColor: '#74b9ff' },
|
||||
buttonText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
||||
forgotLink: { alignItems: 'center' },
|
||||
forgotText: { color: '#0984e3', fontSize: 15, fontWeight: '500' },
|
||||
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 40, paddingTop: 24, borderTopWidth: 1, borderTopColor: '#dfe6e9' },
|
||||
footerText: { color: '#636e72', fontSize: 15 },
|
||||
registerText: { color: '#0984e3', fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
82
app/mainmenu.tsx
Normal file
82
app/mainmenu.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useTheme } from '../themecontext'; // Ajusta o caminho conforme a tua estrutura
|
||||
|
||||
const MainMenu = () => {
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
const themeStyles = {
|
||||
fundo: isDarkMode ? '#121212' : '#f5f5f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#fff' : '#000',
|
||||
textoSecundario: isDarkMode ? '#adb5bd' : 'gray',
|
||||
borda: isDarkMode ? '#333' : '#e0e0e0',
|
||||
iconFundo: isDarkMode ? '#2c2c2c' : '#f0f0f0'
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ id: 1, title: 'Criar Novo Sumário', icon: '📝' },
|
||||
{ id: 2, title: 'Meus Sumários', icon: '📚' },
|
||||
{ id: 3, title: 'Calendário', icon: '📅' },
|
||||
{ id: 4, title: 'Notas', icon: '📊' },
|
||||
{ id: 5, title: 'Horário', icon: '⏰' },
|
||||
{ id: 6, title: 'Perfil', icon: '👤' },
|
||||
{ id: 7, title: 'Configurações', icon: '⚙️' },
|
||||
{ id: 8, title: 'Ajuda', icon: '❓' },
|
||||
];
|
||||
|
||||
const handlePress = (title: string) => {
|
||||
console.log(`Carregaste em: ${title}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
|
||||
|
||||
<View style={[styles.header, { backgroundColor: themeStyles.card, borderBottomColor: themeStyles.borda }]}>
|
||||
<Text style={[styles.greeting, { color: themeStyles.texto }]}>Bem-vindo de volta, Ricardo!</Text>
|
||||
<Text style={[styles.subGreeting, { color: themeStyles.textoSecundario }]}>Desejamos-lhe um bom trabalho!</Text>
|
||||
<Text style={styles.date}>06/01/2026, 14:41:52</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.menuGrid}>
|
||||
{menuItems.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[styles.menuItem, { backgroundColor: themeStyles.card }]}
|
||||
onPress={() => handlePress(item.title)}
|
||||
>
|
||||
<View style={[styles.iconBox, { backgroundColor: themeStyles.iconFundo }]}>
|
||||
<Text style={styles.icon}>{item.icon}</Text>
|
||||
</View>
|
||||
<Text style={[styles.menuText, { color: themeStyles.texto }]}>{item.title}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.bigButton}
|
||||
onPress={() => handlePress('Criar Novo Sumário')}
|
||||
>
|
||||
<Text style={styles.bigButtonText}>Criar Novo Sumário</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
header: { padding: 20, borderBottomWidth: 1 },
|
||||
greeting: { fontSize: 22, fontWeight: 'bold' },
|
||||
subGreeting: { fontSize: 16, marginTop: 5 },
|
||||
date: { fontSize: 14, color: '#0d6efd', marginTop: 10, fontWeight: '500' },
|
||||
menuGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', padding: 10 },
|
||||
menuItem: { width: '48%', borderRadius: 10, padding: 15, marginBottom: 15, alignItems: 'center', elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 3 },
|
||||
iconBox: { width: 50, height: 50, borderRadius: 25, justifyContent: 'center', alignItems: 'center', marginBottom: 10 },
|
||||
icon: { fontSize: 24 },
|
||||
menuText: { fontSize: 14, fontWeight: '600', textAlign: 'center' },
|
||||
bigButton: { backgroundColor: '#0d6efd', margin: 20, padding: 18, borderRadius: 10, alignItems: 'center' },
|
||||
bigButtonText: { color: 'white', fontSize: 18, fontWeight: 'bold' },
|
||||
});
|
||||
|
||||
export default MainMenu;
|
||||
232
app/perfil.tsx
Normal file
232
app/perfil.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet,
|
||||
Text, TouchableOpacity, View
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../themecontext';
|
||||
|
||||
export default function Perfil() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
// Estados para dados dinâmicos e estatísticas
|
||||
const [datas, setDatas] = useState({ inicio: '05/01/2026', fim: '30/05/2026' });
|
||||
const [stats, setStats] = useState({
|
||||
horasConcluidas: 0,
|
||||
faltasTotais: 0,
|
||||
faltasJustificadas: 0,
|
||||
horasFaltam: 300
|
||||
});
|
||||
|
||||
const themeStyles = {
|
||||
fundo: isDarkMode ? '#121212' : '#f1f3f5',
|
||||
card: isDarkMode ? '#1e1e1e' : '#fff',
|
||||
texto: isDarkMode ? '#fff' : '#000',
|
||||
textoSecundario: isDarkMode ? '#adb5bd' : '#6c757d',
|
||||
borda: isDarkMode ? '#333' : '#f1f3f5',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const carregarECalcular = async () => {
|
||||
try {
|
||||
const [config, presencasRaw, faltasRaw, justRaw] = await Promise.all([
|
||||
AsyncStorage.getItem('@dados_estagio'),
|
||||
AsyncStorage.getItem('@presencas'),
|
||||
AsyncStorage.getItem('@faltas'),
|
||||
AsyncStorage.getItem('@justificacoes')
|
||||
]);
|
||||
|
||||
// 1. Carregar Configurações de Datas
|
||||
if (config) {
|
||||
const p = JSON.parse(config);
|
||||
setDatas({
|
||||
inicio: p.inicio || '05/01/2026',
|
||||
fim: p.fim || '30/05/2026'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Calcular estatísticas baseadas nos objetos do calendário
|
||||
const presencas = presencasRaw ? JSON.parse(presencasRaw) : {};
|
||||
const faltas = faltasRaw ? JSON.parse(faltasRaw) : {};
|
||||
const justificacoes = justRaw ? JSON.parse(justRaw) : {};
|
||||
|
||||
const totalDiasPresenca = Object.keys(presencas).length;
|
||||
const totalFaltas = Object.keys(faltas).length;
|
||||
const totalJustificadas = Object.keys(justificacoes).length;
|
||||
|
||||
const horasFeitas = totalDiasPresenca * 7; // 7h por dia
|
||||
const totalHorasEstagio = 300;
|
||||
|
||||
setStats({
|
||||
horasConcluidas: horasFeitas,
|
||||
faltasTotais: totalFaltas,
|
||||
faltasJustificadas: totalJustificadas,
|
||||
horasFaltam: Math.max(0, totalHorasEstagio - horasFeitas)
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Erro ao sincronizar dados do perfil", e);
|
||||
}
|
||||
};
|
||||
carregarECalcular();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: themeStyles.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btnVoltar, { backgroundColor: themeStyles.card }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={24} color={themeStyles.texto} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.tituloGeral, { color: themeStyles.texto }]}>Perfil do Aluno</Text>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* Dados Pessoais - RESTAURADO TOTALMENTE */}
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
|
||||
<Text style={styles.tituloCard}>Dados Pessoais</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Nome</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>Ricardo Gomes</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Idade</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>17 anos</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Residência</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>Junqueira, Vila do Conde</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Telemóvel</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>915783648</Text>
|
||||
</View>
|
||||
|
||||
{/* NOVA SECÇÃO: Assiduidade Dinâmica */}
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
|
||||
<Text style={styles.tituloCard}>Assiduidade</Text>
|
||||
<View style={styles.rowStats}>
|
||||
<View style={styles.itemStat}>
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Faltas Totais</Text>
|
||||
<Text style={[styles.valor, { color: '#dc3545', fontSize: 18 }]}>{stats.faltasTotais}</Text>
|
||||
</View>
|
||||
<View style={styles.itemStat}>
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Justificadas</Text>
|
||||
<Text style={[styles.valor, { color: '#198754', fontSize: 18 }]}>{stats.faltasJustificadas}</Text>
|
||||
</View>
|
||||
<View style={styles.itemStat}>
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario, marginTop: 0 }]}>Ñ Justif.</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto, fontSize: 18 }]}>
|
||||
{stats.faltasTotais - stats.faltasJustificadas}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Empresa de Estágio - RESTAURADO TOTALMENTE */}
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
|
||||
<Text style={styles.tituloCard}>Empresa de Estágio</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Curso</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>Técnico de Informática</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Empresa</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>Tech Solutions, Lda</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Morada</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>Rua das papoilas, nº67</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Tutor</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>Nicolau de Sousa</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Telemóvel</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>917892748</Text>
|
||||
</View>
|
||||
|
||||
{/* Dados do Estágio - HORAS DINÂMICAS */}
|
||||
<View style={[styles.card, { backgroundColor: themeStyles.card }]}>
|
||||
<Text style={styles.tituloCard}>Dados do Estágio</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Início do Estágio</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>{datas.inicio}</Text>
|
||||
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Fim do Estágio</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>{datas.fim}</Text>
|
||||
|
||||
<View style={[styles.estatisticasHoras, { borderBottomColor: themeStyles.borda }]}>
|
||||
<View>
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Totais</Text>
|
||||
<Text style={[styles.valor, { color: themeStyles.texto }]}>300h</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Concluídas</Text>
|
||||
<Text style={[styles.valor, {color: '#198754'}]}>{stats.horasConcluidas}h</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.label, { color: themeStyles.textoSecundario }]}>Faltam</Text>
|
||||
<Text style={[styles.valor, {color: '#dc3545'}]}>{stats.horasFaltam}h</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.labelHorario, { color: themeStyles.texto }]}>Horário Semanal</Text>
|
||||
|
||||
<View style={[styles.tabela, { borderColor: themeStyles.borda }]}>
|
||||
<View style={[styles.linhaTab, { backgroundColor: isDarkMode ? '#2c2c2c' : '#f8f9fa', borderBottomColor: themeStyles.borda }]}>
|
||||
<Text style={[styles.celulaHeader, { color: themeStyles.texto }]}>Período</Text>
|
||||
<Text style={[styles.celulaHeader, { color: themeStyles.texto }]}>Horário</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.linhaTab, { borderBottomColor: themeStyles.borda }]}>
|
||||
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Manhã</Text>
|
||||
<Text style={[styles.celulaValor, { color: themeStyles.texto }]}>09:30 - 13:00</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.linhaTab, { backgroundColor: isDarkMode ? '#252525' : '#fdfcfe', borderBottomColor: themeStyles.borda }]}>
|
||||
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Almoço</Text>
|
||||
<Text style={[styles.celulaValor, { fontWeight: '400', color: themeStyles.textoSecundario }]}>13:00 - 14:30</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.linhaTab, { borderBottomWidth: 0 }]}>
|
||||
<Text style={[styles.celulaLabel, { color: themeStyles.textoSecundario }]}>Tarde</Text>
|
||||
<Text style={[styles.celulaValor, { color: themeStyles.texto }]}>14:30 - 17:30</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.notaTotal}>Total: 7 horas diárias por presença</Text>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
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 },
|
||||
spacer: { width: 40 },
|
||||
container: { padding: 20, gap: 20, paddingBottom: 40 },
|
||||
tituloGeral: { fontSize: 22, fontWeight: 'bold' },
|
||||
card: { padding: 20, borderRadius: 16, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4 },
|
||||
tituloCard: { fontSize: 18, fontWeight: 'bold', color: '#0d6efd', textAlign: 'center', marginBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f3f5', paddingBottom: 8 },
|
||||
label: { marginTop: 12, fontSize: 13 },
|
||||
valor: { fontSize: 16, fontWeight: '600' },
|
||||
labelHorario: { fontSize: 16, fontWeight: 'bold', marginTop: 20, marginBottom: 10, textAlign: 'center' },
|
||||
tabela: { borderWidth: 1, borderRadius: 8, overflow: 'hidden' },
|
||||
linhaTab: { flexDirection: 'row', borderBottomWidth: 1, paddingVertical: 8, alignItems: 'center' },
|
||||
celulaHeader: { flex: 1, fontWeight: 'bold', textAlign: 'center', fontSize: 13 },
|
||||
celulaLabel: { flex: 1, paddingLeft: 12, fontSize: 14 },
|
||||
celulaValor: { flex: 1, textAlign: 'center', fontSize: 14, fontWeight: '600' },
|
||||
notaTotal: { textAlign: 'center', fontSize: 12, color: '#0d6efd', marginTop: 8, fontWeight: '500' },
|
||||
estatisticasHoras: { flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, paddingBottom: 15, marginTop: 5 },
|
||||
rowStats: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 },
|
||||
itemStat: { alignItems: 'center', flex: 1 }
|
||||
});
|
||||
312
app/register.tsx
Normal file
312
app/register.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
|
||||
|
||||
// app/register.tsx - TELA DE CRIAR CONTA (CORRIGIDA)
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform
|
||||
} from 'react-native';
|
||||
import { Link } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function CriarContaScreen() {
|
||||
const [form, setForm] = useState({
|
||||
nome: '',
|
||||
email: '',
|
||||
telefone: '',
|
||||
password: '',
|
||||
confirmarPassword: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setForm(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleRegister = () => {
|
||||
if (!form.nome.trim()) {
|
||||
Alert.alert('Erro', 'Por favor, insira o seu nome');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.email.includes('@')) {
|
||||
Alert.alert('Erro', 'Por favor, insira um email válido');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password.length < 6) {
|
||||
Alert.alert('Erro', 'A senha deve ter pelo menos 6 caracteres');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password !== form.confirmarPassword) {
|
||||
Alert.alert('Erro', 'As senhas não coincidem');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
Alert.alert(
|
||||
'Sucesso!',
|
||||
`Conta criada para ${form.nome}`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
|
||||
setForm({
|
||||
nome: '',
|
||||
email: '',
|
||||
telefone: '',
|
||||
password: '',
|
||||
confirmarPassword: ''
|
||||
});
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<StatusBar style="dark" />
|
||||
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* BOTÃO VOLTAR ATRÁS */}
|
||||
<Link href={"/"} asChild>
|
||||
<TouchableOpacity style={styles.backHeaderButton} disabled={loading}>
|
||||
<Text style={styles.backHeaderText}>←</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
|
||||
{/* CABEÇALHO */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Criar Nova Conta</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Preencha os dados abaixo para se registar
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* FORMULÁRIO */}
|
||||
<View style={styles.formCard}>
|
||||
{/* NOME COMPLETO */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Nome Completo</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu nome completo..."
|
||||
value={form.nome}
|
||||
onChangeText={(text) => handleChange('nome', text)}
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* EMAIL */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu email..."
|
||||
value={form.email}
|
||||
onChangeText={(text) => handleChange('email', text)}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Nº TELEMÓVEL */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Telefone</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira o seu nº telemóvel..."
|
||||
value={form.telefone}
|
||||
onChangeText={(text) => handleChange('telefone', text)}
|
||||
keyboardType="phone-pad"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* PALAVRA-PASSE */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Senha</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Mínimo de 6 caracteres"
|
||||
value={form.password}
|
||||
onChangeText={(text) => handleChange('password', text)}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* CONFIRMAR PALAVRA-PASSE */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Confirmar Senha</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Insira novamente a sua palavra-passe"
|
||||
value={form.confirmarPassword}
|
||||
onChangeText={(text) => handleChange('confirmarPassword', text)}
|
||||
secureTextEntry
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* BOTÃO CRIAR CONTA */}
|
||||
<TouchableOpacity
|
||||
style={[styles.registerButton, loading && styles.buttonDisabled]}
|
||||
onPress={handleRegister}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" />
|
||||
) : (
|
||||
<Text style={styles.registerButtonText}>CRIAR CONTA</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* AVISO */}
|
||||
<View style={styles.termsContainer}>
|
||||
<Text style={styles.termsText}>
|
||||
Ao criar uma conta, concorda com os nossos Termos de Serviço e Política de Privacidade.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// ESTILOS
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
padding: 20,
|
||||
paddingTop: 20,
|
||||
},
|
||||
backHeaderButton: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 10,
|
||||
zIndex: 50,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#f0f0f0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backHeaderText: {
|
||||
fontSize: 24,
|
||||
color: '#007AFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginTop: 50,
|
||||
marginBottom: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#1a1a1a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
},
|
||||
formCard: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
marginBottom: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e9ecef',
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: 6,
|
||||
marginLeft: 4,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
color: '#333',
|
||||
},
|
||||
registerButton: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#7bb8ff',
|
||||
},
|
||||
registerButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
termsContainer: {
|
||||
backgroundColor: '#e8f4ff',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#cce5ff',
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 13,
|
||||
color: '#0066cc',
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
},
|
||||
backButton: {
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
backButtonText: {
|
||||
color: '#007AFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user