updates
This commit is contained in:
5
app.json
5
app.json
@@ -45,11 +45,12 @@
|
||||
"photosPermission": "Permitir que a aplicação aceda às tuas fotos para alterar a foto de perfil.",
|
||||
"cameraPermission": "Permitir que a aplicação utilize a câmara para tirar uma foto de perfil."
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-asset"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
"reactCompiler": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
@@ -12,26 +14,34 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
export default function EmpresaHome() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const { isDarkMode } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [empresaNome, setEmpresaNome] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [empresaNome, setEmpresaNome] = useState('');
|
||||
|
||||
const themeStyles = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
// Paleta EPVC (mantendo a consistência com o Prof)
|
||||
const azulEPVC = '#2390a6';
|
||||
const laranjaEPVC = '#E38E00';
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: azulEPVC,
|
||||
laranja: laranjaEPVC,
|
||||
azulSuave: isDarkMode ? 'rgba(35, 144, 166, 0.15)' : '#E0F2F4',
|
||||
laranjaSuave: isDarkMode ? 'rgba(227, 142, 0, 0.15)' : '#FEF3E6',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: '#2390a6',
|
||||
laranja: '#dd8707',
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
}), [isDarkMode]);
|
||||
|
||||
@@ -46,7 +56,7 @@ export default function EmpresaHome() {
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (empresa) {
|
||||
if (empresa && empresa.nome) {
|
||||
setEmpresaNome(empresa.nome);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -58,127 +68,168 @@ export default function EmpresaHome() {
|
||||
|
||||
useFocusEffect(useCallback(() => { fetchEmpresaInfo(); }, []));
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: themeStyles.fundo }]} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={themeStyles.fundo} />
|
||||
|
||||
{/* CABEÇALHO */}
|
||||
<View style={styles.topBar}>
|
||||
<View style={{ flex: 1, paddingRight: 20 }}>
|
||||
<Text style={[styles.greeting, { color: themeStyles.textoSecundario }]}>Painel da Entidade</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={themeStyles.azul} style={{ marginTop: 5, alignSelf: 'flex-start' }} />
|
||||
) : (
|
||||
<Text style={[styles.title, { color: themeStyles.texto }]} numberOfLines={1}>
|
||||
{empresaNome || 'A carregar...'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.logoutBtn, { borderColor: themeStyles.borda }]} onPress={() => supabase.auth.signOut().then(() => router.replace('/'))}>
|
||||
<Ionicons name="log-out-outline" size={24} color={themeStyles.vermelho} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
const handleLogout = () => {
|
||||
Alert.alert('Terminar Sessão', 'Tens a certeza que pretendes sair?', [
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{ text: 'Sair', style: 'destructive', onPress: () => supabase.auth.signOut().then(() => router.replace('/')) }
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={cores.fundo} />
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
contentContainerStyle={[
|
||||
styles.content,
|
||||
{ paddingBottom: insets.bottom + 40 }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* CARTÃO DE DESTAQUE - PEDIDOS PENDENTES */}
|
||||
<Text style={[styles.sectionLabel, { color: themeStyles.textoSecundario }]}>AÇÃO IMEDIATA</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
style={[styles.heroCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: themeStyles.laranja }]}
|
||||
onPress={() => router.push('/Empresas/pedidos')}
|
||||
>
|
||||
<View style={styles.heroContent}>
|
||||
<View style={[styles.iconWrapperLarge, { backgroundColor: themeStyles.laranja + '15' }]}>
|
||||
<Ionicons name="document-text" size={34} color={themeStyles.laranja} />
|
||||
</View>
|
||||
<View style={styles.heroTextContainer}>
|
||||
<Text style={[styles.heroTitle, { color: themeStyles.texto }]}>Validações Pendentes</Text>
|
||||
<Text style={[styles.heroDesc, { color: themeStyles.textoSecundario }]}>Aprovar presenças e sumários</Text>
|
||||
{/* Cabeçalho */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerRow}>
|
||||
<View style={{ flex: 1, paddingRight: 15 }}>
|
||||
<View style={[styles.badgeEpvc, { backgroundColor: cores.laranjaSuave }]}>
|
||||
<Ionicons name="business" size={12} color={cores.laranja} />
|
||||
<Text style={[styles.badgeTxt, { color: cores.laranja }]}>Painel da Entidade Parceira</Text>
|
||||
</View>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={cores.azul} style={{ marginTop: 8, alignSelf: 'flex-start' }} />
|
||||
) : (
|
||||
<Text style={[styles.name, { color: cores.texto }]} numberOfLines={1}>
|
||||
Olá, {empresaNome || 'Empresa'}
|
||||
</Text>
|
||||
)}
|
||||
<Text style={[styles.subtitle, { color: cores.textoSecundario }]}>Gestão de Estagiários EPVC</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={24} color={themeStyles.textoSecundario} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* SECÇÃO GESTÃO - GRELHA 2 COLUNAS */}
|
||||
<Text style={[styles.sectionLabel, { color: themeStyles.textoSecundario, marginTop: 15 }]}>GESTÃO DE ESTÁGIOS</Text>
|
||||
|
||||
<View style={styles.gridContainer}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
style={[styles.gridCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
|
||||
onPress={() => router.push('/Empresas/alunos')}
|
||||
>
|
||||
<View style={[styles.iconWrapper, { backgroundColor: themeStyles.azul + '15' }]}>
|
||||
<Ionicons name="people" size={26} color={themeStyles.azul} />
|
||||
</View>
|
||||
<Text style={[styles.gridCardTitle, { color: themeStyles.texto }]}>Alunos</Text>
|
||||
<Text style={[styles.gridCardDesc, { color: themeStyles.textoSecundario }]}>Gerir estagiários</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
style={[styles.gridCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
|
||||
onPress={() => router.push('/Empresas/avaliacoesEmpresa')}
|
||||
>
|
||||
<View style={[styles.iconWrapper, { backgroundColor: themeStyles.verde + '15' }]}>
|
||||
<Ionicons name="star" size={26} color={themeStyles.verde} />
|
||||
</View>
|
||||
<Text style={[styles.gridCardTitle, { color: themeStyles.texto }]}>Avaliações</Text>
|
||||
<Text style={[styles.gridCardDesc, { color: themeStyles.textoSecundario }]}>Avaliar estágios</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* DEFINIÇÕES - CARTÃO LARGO INFERIOR */}
|
||||
{/* Criar Registo / Ação Imediata (Hero Card) */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
style={[styles.rowCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda }]}
|
||||
onPress={() => router.push('/Empresas/definicoesEmpresa')}
|
||||
style={[styles.heroCard, { backgroundColor: cores.laranja }]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => router.push('/Empresas/pedidos')}
|
||||
>
|
||||
<View style={[styles.iconWrapperSmall, { backgroundColor: themeStyles.textoSecundario + '15' }]}>
|
||||
<Ionicons name="settings" size={22} color={themeStyles.textoSecundario} />
|
||||
<Ionicons name="document-text" size={120} color="#FFF" style={styles.heroWatermark} />
|
||||
<View style={styles.heroContent}>
|
||||
<View style={{ flex: 1, paddingRight: 10 }}>
|
||||
<Text style={styles.heroTitle}>Validações</Text>
|
||||
<Text style={styles.heroSubtitle}>Aprovar presenças e sumários dos alunos</Text>
|
||||
</View>
|
||||
<View style={styles.heroBtn}>
|
||||
<Ionicons name="arrow-forward" size={20} color={cores.laranja} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.rowTextContainer}>
|
||||
<Text style={[styles.rowTitle, { color: themeStyles.texto }]}>Definições da Conta</Text>
|
||||
<Text style={[styles.rowDesc, { color: themeStyles.textoSecundario }]}>Ajustar dados da empresa e segurança</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color={themeStyles.textoSecundario} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* SECÇÃO: Gestão de Estágios */}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.textoSecundario }]}>Gestão de Estágios</Text>
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="people" title="Alunos" subtitle="Gerir os teus estagiários" onPress={() => router.push('/Empresas/alunos')} cores={cores} corDestaque={cores.azul} />
|
||||
<MenuCard icon="star" title="Avaliações" subtitle="Avaliar desempenho" onPress={() => router.push('/Empresas/avaliacoesEmpresa')} cores={cores} corDestaque={cores.azul} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* SECÇÃO: Sistema */}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: cores.textoSecundario }]}>Sistema</Text>
|
||||
<View style={styles.grid}>
|
||||
<MenuCard icon="settings" title="Definições" subtitle="Ajustar dados da conta" onPress={() => router.push('/Empresas/definicoesEmpresa')} cores={cores} corDestaque={cores.textoSecundario} />
|
||||
<MenuCard icon="log-out" title="Sair" subtitle="Terminar a sessão" onPress={handleLogout} cores={cores} corDestaque={cores.vermelho} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Ionicons name="infinite-outline" size={24} color={cores.borda} style={{ marginBottom: 5 }} />
|
||||
<Text style={[styles.footerTxt, { color: cores.textoSecundario }]}>Estágios+ • Portal da Empresa</Text>
|
||||
</View>
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// COMPONENTE DE CARTÃO REFORMULADO
|
||||
function MenuCard({ icon, title, subtitle, onPress, cores, corDestaque, fullWidth = false }: any) {
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: cores.card,
|
||||
borderColor: cores.borda,
|
||||
width: fullWidth ? '100%' : (width - 56) / 2, // Ajustado ligeiramente o cálculo da largura
|
||||
}
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name={icon} size={80} color={corDestaque} style={[styles.cardWatermark, { opacity: isDarkMode ? 0.05 : 0.03 }]} />
|
||||
|
||||
<View style={[styles.iconWrapper, { backgroundColor: corDestaque + '15' }]}>
|
||||
<Ionicons name={icon} size={22} color={corDestaque} />
|
||||
</View>
|
||||
|
||||
<View style={styles.cardTextContainer}>
|
||||
<Text style={[styles.cardTitle, { color: cores.texto }]}>{title}</Text>
|
||||
<Text style={[styles.cardSubtitle, { color: cores.textoSecundario }]}>{subtitle}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 10, paddingBottom: 15 },
|
||||
greeting: { fontSize: 13, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1 },
|
||||
title: { fontSize: 24, fontWeight: '900', marginTop: 2 },
|
||||
logoutBtn: { width: 45, height: 45, borderRadius: 14, borderWidth: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
content: { padding: 20 },
|
||||
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
sectionLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 1.2, marginBottom: 12, marginLeft: 5 },
|
||||
// Header
|
||||
header: { marginBottom: 25, marginTop: 10 },
|
||||
headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
badgeEpvc: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, alignSelf: 'flex-start', marginBottom: 8 },
|
||||
badgeTxt: { fontSize: 10, fontWeight: '900', letterSpacing: 1, marginLeft: 4 },
|
||||
name: { fontSize: 26, fontWeight: '900', letterSpacing: -0.5 },
|
||||
subtitle: { fontSize: 14, fontWeight: '500', marginTop: 2 },
|
||||
avatarMini: { width: 56, height: 56, borderRadius: 20, justifyContent: 'center', alignItems: 'center', borderWidth: 2 },
|
||||
avatarTxt: { fontSize: 24, fontWeight: '900' },
|
||||
|
||||
heroCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, borderRadius: 24, borderWidth: 1, borderLeftWidth: 6, marginBottom: 25, elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.05, shadowRadius: 10 },
|
||||
heroContent: { flexDirection: 'row', alignItems: 'center', flex: 1 },
|
||||
iconWrapperLarge: { width: 65, height: 65, borderRadius: 20, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
|
||||
heroTextContainer: { flex: 1, paddingRight: 10 },
|
||||
heroTitle: { fontSize: 19, fontWeight: '900', marginBottom: 4 },
|
||||
heroDesc: { fontSize: 13, fontWeight: '600' },
|
||||
// Hero Card (Ação Imediata)
|
||||
heroCard: { borderRadius: 28, padding: 24, minHeight: 140, justifyContent: 'flex-end', overflow: 'hidden', elevation: 8, shadowColor: '#E38E00', shadowOpacity: 0.3, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, marginBottom: 30 },
|
||||
heroWatermark: { position: 'absolute', right: -20, top: -20, opacity: 0.2, transform: [{ rotate: '15deg' }] },
|
||||
heroContent: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' },
|
||||
heroTitle: { color: '#FFF', fontSize: 24, fontWeight: '900', letterSpacing: -0.5 },
|
||||
heroSubtitle: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 13, fontWeight: '600', marginTop: 4 },
|
||||
heroBtn: { width: 44, height: 44, backgroundColor: '#FFF', borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
gridContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20 },
|
||||
gridCard: { width: '48%', padding: 20, borderRadius: 24, borderWidth: 1, alignItems: 'flex-start', elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 },
|
||||
iconWrapper: { padding: 12, borderRadius: 16, marginBottom: 16 },
|
||||
gridCardTitle: { fontSize: 16, fontWeight: '900', marginBottom: 4 },
|
||||
gridCardDesc: { fontSize: 12, fontWeight: '600' },
|
||||
// Secções e Grid
|
||||
sectionContainer: { marginBottom: 25 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12, marginLeft: 4 },
|
||||
grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
|
||||
|
||||
// Cartões Menores
|
||||
card: {
|
||||
borderRadius: 24,
|
||||
padding: 18,
|
||||
marginBottom: 16,
|
||||
borderWidth: 1,
|
||||
overflow: 'hidden',
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 10,
|
||||
shadowOffset: { width: 0, height: 4 }
|
||||
},
|
||||
cardWatermark: { position: 'absolute', right: -15, bottom: -15, transform: [{ rotate: '-10deg' }] },
|
||||
iconWrapper: { width: 42, height: 42, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginBottom: 16 },
|
||||
cardTextContainer: { marginTop: 'auto' },
|
||||
cardTitle: { fontSize: 16, fontWeight: '800', letterSpacing: -0.3 },
|
||||
cardSubtitle: { fontSize: 12, marginTop: 2, fontWeight: '600' },
|
||||
|
||||
rowCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, borderWidth: 1, marginTop: 5 },
|
||||
iconWrapperSmall: { width: 45, height: 45, borderRadius: 14, justifyContent: 'center', alignItems: 'center', marginRight: 15 },
|
||||
rowTextContainer: { flex: 1 },
|
||||
rowTitle: { fontSize: 15, fontWeight: '800', marginBottom: 2 },
|
||||
rowDesc: { fontSize: 12, fontWeight: '600' }
|
||||
// Footer
|
||||
footer: { marginTop: 20, alignItems: 'center', paddingBottom: 20 },
|
||||
footerTxt: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1.2 }
|
||||
});
|
||||
@@ -4,17 +4,17 @@ import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
@@ -55,7 +55,6 @@ export default function GestaoAlunos() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🐅 GRAÇAS AO TIGRE, BASTA LER A COLUNA horas_concluidas DIRETO DA BASE DE DADOS!
|
||||
const { data: estagios, error } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
@@ -72,7 +71,6 @@ export default function GestaoAlunos() {
|
||||
if (error) throw error;
|
||||
|
||||
const formatados = estagios?.map((estagio: any) => {
|
||||
// 🟢 TRUQUE ANTI-ERRO: Tira o aluno da lista se o Supabase mandar em formato Array
|
||||
const alunoObj = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos;
|
||||
|
||||
return {
|
||||
@@ -82,7 +80,7 @@ export default function GestaoAlunos() {
|
||||
data_inicio: estagio.data_inicio,
|
||||
data_fim: estagio.data_fim,
|
||||
horas_totais: estagio.horas_totais || 0,
|
||||
horas_concluidas: estagio.horas_concluidas || 0, // <-- LIDO DIRETO DA TABELA!
|
||||
horas_concluidas: estagio.horas_concluidas || 0,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
@@ -159,8 +157,13 @@ export default function GestaoAlunos() {
|
||||
const progressoPercent = aluno.horas_totais > 0 ? (aluno.horas_concluidas / aluno.horas_totais) * 100 : 0;
|
||||
|
||||
return (
|
||||
<View
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => router.push({
|
||||
pathname: '/Empresas/detalhesAluno',
|
||||
params: { estagio_id: aluno.id_estagio, aluno_nome: aluno.aluno_nome }
|
||||
})}
|
||||
style={[styles.card, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: themeStyles.azul }]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
@@ -199,7 +202,7 @@ export default function GestaoAlunos() {
|
||||
<View style={[styles.progressBarFill, { backgroundColor: themeStyles.azul, width: `${Math.min(progressoPercent, 100)}%` }]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
// app/Empresas/avaliacoesEmpresa.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function AvaliacoesEmpresaLista() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
const [estagios, setEstagios] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azul: '#2390a6',
|
||||
verde: '#10B981',
|
||||
laranja: '#E38E00',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchAlunos = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data: empresa } = await supabase
|
||||
.from('empresas')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (!empresa) return;
|
||||
|
||||
// Vai buscar os estagios desta empresa e cruza com os nomes dos alunos
|
||||
const { data, error } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
id,
|
||||
nota_final,
|
||||
alunos (id, nome)
|
||||
`)
|
||||
.eq('empresa_id', empresa.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (data) {
|
||||
// Formatar os dados para a lista, lidando com o picuinhas do TypeScript
|
||||
const listaFormatada = data.map((estagio: any) => {
|
||||
// Extrair o aluno em segurança, quer venha como objeto ou como lista (Array)
|
||||
const aluno = Array.isArray(estagio.alunos) ? estagio.alunos[0] : estagio.alunos;
|
||||
|
||||
return {
|
||||
estagio_id: estagio.id,
|
||||
aluno_id: aluno?.id,
|
||||
aluno_nome: aluno?.nome || 'Aluno Desconhecido',
|
||||
nota: estagio.nota_final,
|
||||
avaliado: estagio.nota_final !== null && estagio.nota_final !== undefined
|
||||
};
|
||||
});
|
||||
setEstagios(listaFormatada);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(useCallback(() => { fetchAlunos(); }, []));
|
||||
|
||||
const renderItem = ({ item }: any) => (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}
|
||||
onPress={() => router.push({
|
||||
pathname: '/Empresas/fichaAvaliacao',
|
||||
params: { estagio_id: item.estagio_id, aluno_nome: item.aluno_nome, nota_atual: item.nota }
|
||||
})}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azul + '15' }]}>
|
||||
<Ionicons name="person" size={20} color={cores.azul} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.alunoName, { color: cores.texto }]}>{item.aluno_nome}</Text>
|
||||
<Text style={[styles.statusText, { color: item.avaliado ? cores.verde : cores.laranja }]}>
|
||||
{item.avaliado ? '✓ Avaliação Concluída' : 'Aguardando Avaliação'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.avaliado ? (
|
||||
<View style={[styles.gradeBadge, { backgroundColor: cores.verde + '15' }]}>
|
||||
<Text style={[styles.gradeText, { color: cores.verde }]}>{item.nota} / 20</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Ionicons name="chevron-forward" size={24} color={cores.textoSecundario} />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.btnVoltar} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Avaliar Alunos</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={estagios}
|
||||
keyExtractor={(item) => item.estagio_id}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyBox}>
|
||||
<Ionicons name="folder-open-outline" size={48} color={cores.textoSecundario} style={{ opacity: 0.5 }} />
|
||||
<Text style={[styles.emptyText, { color: cores.textoSecundario }]}>Nenhum estagiário associado à sua entidade neste momento.</Text>
|
||||
</View>
|
||||
}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingTop: 10, paddingBottom: 20 },
|
||||
btnVoltar: { padding: 5, marginLeft: -5 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900' },
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
listContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
|
||||
card: { padding: 18, borderRadius: 24, borderWidth: 1, marginBottom: 15, elevation: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.03, shadowRadius: 5 },
|
||||
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
iconBox: { width: 44, height: 44, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
alunoName: { fontSize: 16, fontWeight: '800', marginBottom: 2 },
|
||||
statusText: { fontSize: 12, fontWeight: '700' },
|
||||
|
||||
gradeBadge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12 },
|
||||
gradeText: { fontSize: 14, fontWeight: '900' },
|
||||
|
||||
emptyBox: { alignItems: 'center', marginTop: 60, paddingHorizontal: 30 },
|
||||
emptyText: { textAlign: 'center', marginTop: 15, fontSize: 15, fontWeight: '500', lineHeight: 22 }
|
||||
});
|
||||
252
app/Empresas/detalhesAluno.tsx
Normal file
252
app/Empresas/detalhesAluno.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
// app/Empresa/detalhesAluno.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function DetalhesAlunoEmpresa() {
|
||||
const { isDarkMode } = useTheme();
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams();
|
||||
|
||||
const estagio_id = Array.isArray(params.estagio_id) ? params.estagio_id[0] : params.estagio_id;
|
||||
|
||||
const [estagio, setEstagio] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#0D2235',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azulMarinho: '#003049',
|
||||
verdeAgua: '#71BEB3',
|
||||
laranja: '#F18721',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const fetchDetalhes = async (isManualRefresh = false) => {
|
||||
if (!estagio_id) return;
|
||||
if (!isManualRefresh) setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Buscar dados do Estágio e do Aluno (Removido o campo cargo)
|
||||
const { data, error } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
id, data_inicio, data_fim, horas_totais, horas_concluidas,
|
||||
nota_final, avaliacao_url,
|
||||
alunos (id, nome, turma_curso, n_escola, profile_id, ano)
|
||||
`)
|
||||
.eq('id', estagio_id)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const alunoData = Array.isArray(data.alunos) ? data.alunos[0] : data.alunos;
|
||||
|
||||
// 2. Buscar detalhes no Profile (Schema: residencia, data_nascimento, telefone, email)
|
||||
let infoExtra = { telefone: 'N/A', email: 'N/A', residencia: 'N/A', d_nasc: 'N/A' };
|
||||
|
||||
if (alunoData?.profile_id) {
|
||||
const { data: profile, error: profError } = await supabase
|
||||
.from('profiles')
|
||||
.select('telefone, email, residencia, data_nascimento')
|
||||
.eq('id', alunoData.profile_id)
|
||||
.single();
|
||||
|
||||
if (!profError && profile) {
|
||||
infoExtra = {
|
||||
telefone: profile.telefone || 'N/A',
|
||||
email: profile.email || 'N/A',
|
||||
residencia: profile.residencia || 'N/A',
|
||||
d_nasc: profile.data_nascimento || 'N/A'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setEstagio({ ...data, aluno: alunoData, infoExtra });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Alert.alert('Erro', 'Falha ao carregar dossiê do aluno.');
|
||||
} finally {
|
||||
if (!isManualRefresh) setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(useCallback(() => { fetchDetalhes(); }, [estagio_id]));
|
||||
|
||||
const formatarData = (dataStr: string) => {
|
||||
if (!dataStr || dataStr === 'N/A') return 'N/A';
|
||||
const d = new Date(dataStr);
|
||||
return d.toLocaleDateString('pt-PT');
|
||||
};
|
||||
|
||||
if (loading && !refreshing) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
|
||||
<View style={styles.centerBox}><ActivityIndicator size="large" color={cores.azulMarinho} /></View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const progresso = estagio?.horas_totais > 0 ? (estagio.horas_concluidas / estagio.horas_totais) * 100 : 0;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()}><Ionicons name="arrow-back" size={26} color={cores.texto} /></TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Dossiê do Estagiário</Text>
|
||||
<View style={{ width: 26 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); fetchDetalhes(true); }} />}
|
||||
>
|
||||
{/* CABEÇALHO DE IDENTIFICAÇÃO */}
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda, alignItems: 'center' }]}>
|
||||
<View style={[styles.avatarCirculo, { backgroundColor: cores.azulMarinho }]}>
|
||||
<Text style={styles.avatarLetra}>{estagio.aluno?.nome?.charAt(0) || '?'}</Text>
|
||||
</View>
|
||||
<Text style={[styles.nomeAluno, { color: cores.texto }]}>{estagio.aluno?.nome}</Text>
|
||||
<Text style={[styles.subAnotacao, { color: cores.verdeAgua }]}>{estagio.aluno?.turma_curso}</Text>
|
||||
</View>
|
||||
|
||||
{/* INFO ACADÉMICA */}
|
||||
<Text style={styles.sectionTitle}>Informação Académica</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.gridInfo}>
|
||||
<View style={styles.gridItem}>
|
||||
<Text style={styles.labelMini}>Nº ESCOLA</Text>
|
||||
<Text style={[styles.valorMedio, { color: cores.texto }]}>{estagio.aluno?.n_escola || '--'}</Text>
|
||||
</View>
|
||||
<View style={styles.gridItem}>
|
||||
<Text style={styles.labelMini}>ANO LETIVO</Text>
|
||||
<Text style={[styles.valorMedio, { color: cores.texto }]}>{estagio.aluno?.ano || '--'}º Ano</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* CONTACTOS E MORADA */}
|
||||
<Text style={styles.sectionTitle}>Contactos e Localização</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.linhaDetalhe}>
|
||||
<Ionicons name="call-outline" size={18} color={cores.azulMarinho} />
|
||||
<Text style={[styles.textoDetalhe, { color: cores.texto }]}>{estagio.infoExtra.telefone}</Text>
|
||||
</View>
|
||||
<View style={styles.linhaDetalhe}>
|
||||
<Ionicons name="mail-outline" size={18} color={cores.azulMarinho} />
|
||||
<Text style={[styles.textoDetalhe, { color: cores.texto }]}>{estagio.infoExtra.email}</Text>
|
||||
</View>
|
||||
<View style={styles.linhaDetalhe}>
|
||||
<Ionicons name="location-outline" size={18} color={cores.azulMarinho} />
|
||||
<Text style={[styles.textoDetalhe, { color: cores.texto }]}>{estagio.infoExtra.residencia}</Text>
|
||||
</View>
|
||||
<View style={styles.linhaDetalhe}>
|
||||
<Ionicons name="calendar-outline" size={18} color={cores.azulMarinho} />
|
||||
<Text style={[styles.textoDetalhe, { color: cores.texto }]}>Nascido a: {formatarData(estagio.infoExtra.d_nasc)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* PROGRESSO */}
|
||||
<Text style={styles.sectionTitle}>Estado do Estágio</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.progressoHeader}>
|
||||
<Text style={[styles.labelMini, { marginBottom: 0 }]}>HORAS REALIZADAS</Text>
|
||||
<Text style={{ fontWeight: 'bold', color: cores.texto }}>{estagio.horas_concluidas}h / {estagio.horas_totais}h</Text>
|
||||
</View>
|
||||
<View style={styles.barBg}>
|
||||
<View style={[styles.barFill, { width: `${Math.min(progresso, 100)}%`, backgroundColor: cores.verdeAgua }]} />
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 15 }}>
|
||||
<View>
|
||||
<Text style={styles.labelMini}>DATA INÍCIO</Text>
|
||||
<Text style={[styles.valorData, { color: cores.texto }]}>{formatarData(estagio.data_inicio)}</Text>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Text style={styles.labelMini}>FIM PREVISTO</Text>
|
||||
<Text style={[styles.valorData, { color: cores.texto }]}>{formatarData(estagio.data_fim)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* AVALIAÇÃO */}
|
||||
<Text style={styles.sectionTitle}>Documentação Oficial</Text>
|
||||
{estagio.nota_final ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.cardAvaliado, { backgroundColor: cores.azulMarinho }]}
|
||||
onPress={() => WebBrowser.openBrowserAsync(estagio.avaliacao_url)}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.textoBranco}>Estagiário Avaliado</Text>
|
||||
<Text style={styles.notaTexto}>Classificação: {estagio.nota_final} Valores</Text>
|
||||
</View>
|
||||
<Ionicons name="document-text" size={30} color="#FFF" />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={[styles.btnAvaliar, { backgroundColor: cores.laranja }]}
|
||||
onPress={() => router.push({
|
||||
pathname: '/Empresas/fichaAvaliacao',
|
||||
params: { estagio_id: estagio.id, aluno_nome: estagio.aluno?.nome }
|
||||
})}
|
||||
>
|
||||
<Ionicons name="create-outline" size={22} color="#FFF" />
|
||||
<Text style={styles.btnTexto}>Realizar Avaliação Final</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
centerBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '900' },
|
||||
scrollContent: { padding: 20, paddingBottom: 40 },
|
||||
card: { padding: 20, borderRadius: 20, borderWidth: 1, marginBottom: 15 },
|
||||
avatarCirculo: { width: 70, height: 70, borderRadius: 35, justifyContent: 'center', alignItems: 'center', marginBottom: 10 },
|
||||
avatarLetra: { color: '#FFF', fontSize: 28, fontWeight: 'bold' },
|
||||
nomeAluno: { fontSize: 22, fontWeight: '900', textAlign: 'center' },
|
||||
subAnotacao: { fontSize: 14, fontWeight: '700', textAlign: 'center', marginTop: 2 },
|
||||
sectionTitle: { fontSize: 12, fontWeight: '900', textTransform: 'uppercase', color: '#64748B', marginBottom: 8, marginLeft: 5, letterSpacing: 1 },
|
||||
gridInfo: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
gridItem: { flex: 1 },
|
||||
labelMini: { fontSize: 10, color: '#94A3B8', fontWeight: '800', marginBottom: 4 },
|
||||
valorMedio: { fontSize: 15, fontWeight: '700' },
|
||||
valorData: { fontSize: 13, fontWeight: '700' },
|
||||
linhaDetalhe: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
||||
textoDetalhe: { marginLeft: 12, fontSize: 14, fontWeight: '500' },
|
||||
progressoHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 },
|
||||
barBg: { height: 10, backgroundColor: '#E2E8F0', borderRadius: 5, overflow: 'hidden' },
|
||||
barFill: { height: '100%', borderRadius: 5 },
|
||||
cardAvaliado: { padding: 20, borderRadius: 20, flexDirection: 'row', alignItems: 'center', marginBottom: 20 },
|
||||
textoBranco: { color: '#FFF', fontSize: 14, fontWeight: '600' },
|
||||
notaTexto: { color: '#FFF', fontSize: 18, fontWeight: '900' },
|
||||
btnAvaliar: { padding: 18, borderRadius: 20, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 10 },
|
||||
btnTexto: { color: '#FFF', fontSize: 16, fontWeight: '900' }
|
||||
});
|
||||
459
app/Empresas/fichaAvaliacao.tsx
Normal file
459
app/Empresas/fichaAvaliacao.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
// app/Empresas/fichaAvaliacao.tsx
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Asset } from 'expo-asset';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Print from 'expo-print';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useTheme } from '../../themecontext';
|
||||
|
||||
export default function FichaAvaliacao() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode } = useTheme();
|
||||
const params = useLocalSearchParams();
|
||||
|
||||
const estagio_id = Array.isArray(params.estagio_id) ? params.estagio_id[0] : params.estagio_id;
|
||||
const aluno_nome = Array.isArray(params.aluno_nome) ? params.aluno_nome[0] : params.aluno_nome;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// As 10 Perguntas Oficiais das Escolas Profissionais
|
||||
const [criterios, setCriterios] = useState({
|
||||
assiduidade: 0,
|
||||
relacionamento: 0,
|
||||
responsabilidade: 0,
|
||||
iniciativa: 0,
|
||||
adaptacao: 0,
|
||||
conhecimentos: 0,
|
||||
qualidade: 0,
|
||||
empenho: 0,
|
||||
equipamentos: 0,
|
||||
seguranca: 0,
|
||||
});
|
||||
|
||||
const [notaFinal, setNotaFinal] = useState('');
|
||||
const [observacoes, setObservacoes] = useState('');
|
||||
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0A0A0A' : '#F4F7FA',
|
||||
card: isDarkMode ? '#161618' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#0D2235',
|
||||
textoSecundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
azulMarinho: '#003049',
|
||||
verdeAgua: '#71BEB3',
|
||||
laranja: '#F18721',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const getBase64Image = async (imageModule: any) => {
|
||||
try {
|
||||
const asset = Asset.fromModule(imageModule);
|
||||
await asset.downloadAsync();
|
||||
const fileUri = asset.localUri || asset.uri;
|
||||
if (!fileUri) return "";
|
||||
const base64 = await FileSystem.readAsStringAsync(fileUri, { encoding: 'base64' });
|
||||
return `data:image/png;base64,${base64}`;
|
||||
} catch (e) {
|
||||
console.error("Erro no Base64:", e);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const ClassificacaoRow = ({ label, field }: { label: string, field: keyof typeof criterios }) => (
|
||||
<View style={styles.criterioContainer}>
|
||||
<Text style={[styles.criterioLabel, { color: cores.texto }]}>{label}</Text>
|
||||
<View style={styles.botoesContainer}>
|
||||
{[1, 2, 3, 4, 5].map((num) => {
|
||||
const selecionado = criterios[field] === num;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={num}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => setCriterios({ ...criterios, [field]: num })}
|
||||
style={[
|
||||
styles.botaoNota,
|
||||
{
|
||||
borderColor: selecionado ? cores.azulMarinho : cores.borda,
|
||||
backgroundColor: selecionado ? cores.azulMarinho : cores.card,
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Text style={[
|
||||
styles.botaoTexto,
|
||||
{ color: selecionado ? '#FFF' : cores.textoSecundario }
|
||||
]}>
|
||||
{num}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const submeterAvaliacao = async () => {
|
||||
const faltamRespostas = Object.values(criterios).some(val => val === 0);
|
||||
if (faltamRespostas) {
|
||||
Alert.alert('Aviso', 'Preencha todos os 10 parâmetros antes de gerar o PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
const notaNum = parseInt(notaFinal);
|
||||
if (isNaN(notaNum) || notaNum < 0 || notaNum > 20) {
|
||||
Alert.alert('Erro', 'Nota final inválida (0-20).');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. DADOS DO SUPABASE: Substituído "cargo" por "horas_totais" no destaque
|
||||
const { data: infoEstagio, error: infoError } = await supabase
|
||||
.from('estagios')
|
||||
.select(`
|
||||
data_inicio,
|
||||
data_fim,
|
||||
horas_totais,
|
||||
empresas (nome, tutor_nome),
|
||||
alunos (nome, n_escola, turma_curso)
|
||||
`)
|
||||
.eq('id', estagio_id)
|
||||
.single();
|
||||
|
||||
if (infoError) console.warn("Erro a buscar dados:", infoError);
|
||||
|
||||
const empresaData = Array.isArray(infoEstagio?.empresas) ? infoEstagio?.empresas[0] : infoEstagio?.empresas;
|
||||
const alunoData = Array.isArray(infoEstagio?.alunos) ? infoEstagio?.alunos[0] : infoEstagio?.alunos;
|
||||
|
||||
const nomeEmpresa = empresaData?.nome || 'Não definida';
|
||||
const tutorEmpresa = empresaData?.tutor_nome || 'N/A';
|
||||
|
||||
const nomeAlunoExtracted = alunoData?.nome || aluno_nome || 'N/A';
|
||||
const cursoAluno = alunoData?.turma_curso || 'N/A';
|
||||
const numeroAluno = alunoData?.n_escola || '--';
|
||||
|
||||
const horasTotais = infoEstagio?.horas_totais ? `${infoEstagio.horas_totais} Horas` : 'N/A';
|
||||
const dataInicioFormatada = infoEstagio?.data_inicio ? new Date(infoEstagio.data_inicio).toLocaleDateString('pt-PT') : 'N/A';
|
||||
const dataFimFormatada = infoEstagio?.data_fim ? new Date(infoEstagio.data_fim).toLocaleDateString('pt-PT') : 'N/A';
|
||||
|
||||
const logoEPVC_b64 = await getBase64Image(require('../../assets/images/logoepvc2.png'));
|
||||
const logoEstagios_b64 = await getBase64Image(require('../../assets/images/logo.png'));
|
||||
const bannerEU_b64 = await getBase64Image(require('../../assets/images/logoepvc.png'));
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-PT">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 0; }
|
||||
html, body { height: 99%; overflow: hidden; }
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 12mm 15mm;
|
||||
color: #1E293B;
|
||||
line-height: 1.2;
|
||||
background-color: #fff;
|
||||
font-size: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header-table { width: 100%; border-bottom: 2px solid #003049; padding-bottom: 5px; margin-bottom: 8px; }
|
||||
.header-table td { vertical-align: middle; }
|
||||
|
||||
.logo-epvc { width: 110px; display: block; }
|
||||
.logo-estagios { width: 160px; display: block; float: right; margin-top: -5px; margin-right: -5px; }
|
||||
|
||||
.header-center { text-align: center; }
|
||||
.header-center h1 { color: #003049; margin: 0; font-size: 15px; font-weight: 900; text-transform: uppercase; }
|
||||
.header-center h2 { color: #F18721; margin: 2px 0 0; font-size: 9px; font-weight: 800; text-transform: uppercase; }
|
||||
|
||||
.info-table { width: 100%; border-collapse: collapse; margin-bottom: 10px; font-size: 9px; border: 1px solid #CBD5E1; }
|
||||
.info-table td { padding: 4px 6px; border: 1px solid #CBD5E1; }
|
||||
.info-table td.label { background-color: #F8FAFC; font-weight: bold; width: 15%; color: #003049; }
|
||||
.info-table td.value { width: 35%; color: #334155; }
|
||||
|
||||
.section-title {
|
||||
background-color: #003049;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.eval-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 9px; }
|
||||
.eval-table th { background-color: #F1F5F9; color: #003049; font-weight: bold; padding: 4px 8px; border: 1px solid #CBD5E1; text-align: left; }
|
||||
.eval-table td { border: 1px solid #CBD5E1; padding: 3px 8px; vertical-align: middle; color: #334155; }
|
||||
.eval-table th.score, .eval-table td.score { text-align: center; width: 60px; }
|
||||
.eval-table td.score { font-size: 11px; font-weight: bold; color: #003049; background-color: #F8FAFC; }
|
||||
.eval-desc { font-size: 8px; color: #64748B; display: block; margin-top: 1px; font-style: italic; }
|
||||
|
||||
.parecer-box { border: 1px solid #CBD5E1; padding: 6px; min-height: 35px; font-size: 9px; color: #334155; background-color: #F8FAFC; border-radius: 3px; margin-bottom: 10px; }
|
||||
|
||||
.final-score-container { text-align: right; margin-bottom: 5px; }
|
||||
.final-score-box { display: inline-block; border: 2px solid #003049; padding: 6px 15px; border-radius: 4px; font-size: 11px; font-weight: bold; color: #003049; background-color: #F8FAFC; }
|
||||
.final-score-box span { color: #F18721; font-size: 14px; margin-left: 8px; font-weight: 900; }
|
||||
|
||||
.footer { text-align: center; padding-top: 5px; border-top: 1px solid #E2E8F0; width: 100%; position: absolute; bottom: 10mm; left: 0; right: 0; margin: 0 auto; width: calc(100% - 30mm); }
|
||||
.banner-img { max-width: 100%; height: auto; max-height: 30px; margin-bottom: 3px; }
|
||||
.footer-text { font-size: 8px; color: #94A3B8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<table class="header-table">
|
||||
<tr>
|
||||
<td style="width: 25%;"><img src="${logoEPVC_b64}" class="logo-epvc" /></td>
|
||||
<td style="width: 50%;" class="header-center">
|
||||
<h1>Ficha de Avaliação de Estágio</h1>
|
||||
<h2>Formação em Contexto de Trabalho</h2>
|
||||
</td>
|
||||
<td style="width: 25%; text-align: right;"><img src="${logoEstagios_b64}" class="logo-estagios" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td class="label">Estagiário:</td>
|
||||
<td class="value"><strong>${nomeAlunoExtracted}</strong> (Nº ${numeroAluno})</td>
|
||||
<td class="label">Turma/Curso:</td>
|
||||
<td class="value">${cursoAluno}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Entidade:</td>
|
||||
<td class="value"><strong>${nomeEmpresa}</strong></td>
|
||||
<td class="label">Tutor(a):</td>
|
||||
<td class="value">${tutorEmpresa}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Carga Horária:</td>
|
||||
<td class="value"><strong>${horasTotais}</strong></td>
|
||||
<td class="label">Período:</td>
|
||||
<td class="value">${dataInicioFormatada} a ${dataFimFormatada}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="section-title">I. Parâmetros Comportamentais</div>
|
||||
<table class="eval-table">
|
||||
<tr>
|
||||
<th>Critérios de Avaliação</th>
|
||||
<th class="score">Class.</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>1. Assiduidade e Pontualidade</strong><span class="eval-desc">Cumprimento de horários e justificação de ausências.</span></td>
|
||||
<td class="score">${criterios.assiduidade}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>2. Relacionamento Interpessoal</strong><span class="eval-desc">Integração na equipa e trato com superiores e colegas.</span></td>
|
||||
<td class="score">${criterios.relacionamento}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>3. Responsabilidade e Organização</strong><span class="eval-desc">Cuidado com o material, posto de trabalho e planeamento.</span></td>
|
||||
<td class="score">${criterios.responsabilidade}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>4. Iniciativa e Autonomia</strong><span class="eval-desc">Ação proativa e resolução de problemas sem supervisão.</span></td>
|
||||
<td class="score">${criterios.iniciativa}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>5. Adaptação a Novas Tarefas</strong><span class="eval-desc">Facilidade e rapidez de aprendizagem perante novos desafios.</span></td>
|
||||
<td class="score">${criterios.adaptacao}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="section-title">II. Parâmetros Técnicos e Profissionais</div>
|
||||
<table class="eval-table">
|
||||
<tr>
|
||||
<th>Critérios de Avaliação</th>
|
||||
<th class="score">Class.</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>6. Aplicação de Conhecimentos</strong><span class="eval-desc">Utilização prática dos conhecimentos adquiridos no curso.</span></td>
|
||||
<td class="score">${criterios.conhecimentos}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>7. Qualidade e Rigor</strong><span class="eval-desc">Atenção ao detalhe, brio profissional e ausência de erros.</span></td>
|
||||
<td class="score">${criterios.qualidade}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>8. Interesse e Empenho</strong><span class="eval-desc">Motivação, dedicação e vontade contínua de evoluir.</span></td>
|
||||
<td class="score">${criterios.empenho}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>9. Uso de Equipamentos e Ferramentas</strong><span class="eval-desc">Destreza, manuseamento correto e cuidado técnico.</span></td>
|
||||
<td class="score">${criterios.equipamentos}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>10. Segurança e Higiene</strong><span class="eval-desc">Cumprimento estrito das normas de segurança no trabalho.</span></td>
|
||||
<td class="score">${criterios.seguranca}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="section-title">III. Parecer Global da Entidade</div>
|
||||
<div class="parecer-box">
|
||||
${observacoes ? observacoes.replace(/\n/g, '<br/>') : '<em>Nenhum parecer qualitativo submetido.</em>'}
|
||||
</div>
|
||||
|
||||
<div class="final-score-container">
|
||||
<div class="final-score-box">
|
||||
CLASSIFICAÇÃO FINAL ATRIBUÍDA: <span>${notaFinal} / 20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<img src="${bannerEU_b64}" class="banner-img" />
|
||||
<div class="footer-text">
|
||||
Documento gerado digitalmente pela Entidade de Acolhimento via plataforma Estágios+ EPVC.<br/>
|
||||
Data de emissão: ${new Date().toLocaleDateString('pt-PT')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const { uri } = await Print.printToFileAsync({ html: htmlContent });
|
||||
|
||||
const fileName = `avaliacao_${estagio_id}_${Date.now()}.pdf`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', {
|
||||
uri: Platform.OS === 'android' ? uri : uri.replace('file://', ''),
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
} as any);
|
||||
|
||||
const { error: uploadError } = await supabase.storage.from('avaliacoes').upload(fileName, formData);
|
||||
if (uploadError) throw uploadError;
|
||||
|
||||
const { data: urlData } = supabase.storage.from('avaliacoes').getPublicUrl(fileName);
|
||||
const { error: dbError } = await supabase
|
||||
.from('estagios')
|
||||
.update({ nota_final: notaNum, avaliacao_url: urlData.publicUrl })
|
||||
.eq('id', estagio_id);
|
||||
|
||||
if (dbError) throw dbError;
|
||||
|
||||
Alert.alert('Sucesso', 'Ficha oficial gerada com sucesso e anexada ao processo do aluno.');
|
||||
if (await Sharing.isAvailableAsync()) await Sharing.shareAsync(uri);
|
||||
router.back();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Alert.alert('Erro', 'Ocorreu uma falha crítica na geração do PDF.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: cores.fundo }} edges={['top', 'left', 'right']}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={{ padding: 5 }}><Ionicons name="arrow-back" size={24} color={cores.texto} /></TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: cores.texto }]}>Ficha de Avaliação</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={[styles.alunoCard, { backgroundColor: cores.verdeAgua + '15', borderColor: cores.verdeAgua + '40' }]}>
|
||||
<Ionicons name="person-circle-outline" size={32} color={cores.azulMarinho} />
|
||||
<View>
|
||||
<Text style={[styles.alunoCardText, { color: cores.azulMarinho }]}>{aluno_nome}</Text>
|
||||
<Text style={{ fontSize: 12, color: cores.azulMarinho, opacity: 0.7 }}>Avalie os 10 parâmetros oficiais.</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionAppTitle}>Critérios Comportamentais</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<ClassificacaoRow label="Assiduidade e Pontualidade" field="assiduidade" />
|
||||
<ClassificacaoRow label="Relacionamento Interpessoal" field="relacionamento" />
|
||||
<ClassificacaoRow label="Responsabilidade e Organização" field="responsabilidade" />
|
||||
<ClassificacaoRow label="Iniciativa e Autonomia" field="iniciativa" />
|
||||
<ClassificacaoRow label="Adaptação a Novas Tarefas" field="adaptacao" />
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionAppTitle}>Critérios Técnicos</Text>
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<ClassificacaoRow label="Aplicação de Conhecimentos" field="conhecimentos" />
|
||||
<ClassificacaoRow label="Qualidade e Rigor" field="qualidade" />
|
||||
<ClassificacaoRow label="Interesse e Empenho" field="empenho" />
|
||||
<ClassificacaoRow label="Uso de Equipamentos/Ferramentas" field="equipamentos" />
|
||||
<ClassificacaoRow label="Segurança e Higiene" field="seguranca" />
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionAppTitle}>Classificação Final (0-20)</Text>
|
||||
<View style={[styles.notaCard, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.notaLabel, { color: cores.texto }]}>Nota Quantitativa</Text>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.gradeInput, { color: cores.laranja, borderColor: cores.laranja + '50', backgroundColor: cores.laranja + '05' }]}
|
||||
keyboardType="numeric" maxLength={2} placeholder="--" placeholderTextColor={cores.textoSecundario} value={notaFinal} onChangeText={setNotaFinal}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionAppTitle}>Parecer Qualitativo</Text>
|
||||
<TextInput
|
||||
style={[styles.textArea, { backgroundColor: cores.card, borderColor: cores.borda, color: cores.texto }]}
|
||||
multiline placeholder="Deixe um comentário institucional sobre a prestação do aluno..." placeholderTextColor={cores.textoSecundario}
|
||||
value={observacoes} onChangeText={setObservacoes} textAlignVertical="top"
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.footerBtnContainer, { backgroundColor: cores.fundo }]}>
|
||||
<TouchableOpacity style={[styles.btnSubmit, { backgroundColor: cores.azulMarinho }]} onPress={submeterAvaliacao} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color="#FFF" /> : <Text style={styles.btnSubmitText}>Finalizar e Gerar Documento</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 20 },
|
||||
headerTitle: { fontSize: 20, fontWeight: '900' },
|
||||
scrollContent: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
alunoCard: { flexDirection: 'row', alignItems: 'center', padding: 18, borderRadius: 20, borderWidth: 1, marginBottom: 25, gap: 12 },
|
||||
alunoCardText: { fontSize: 17, fontWeight: '900' },
|
||||
sectionAppTitle: { fontSize: 13, fontWeight: '900', color: '#64748B', marginBottom: 12, marginLeft: 5, textTransform: 'uppercase' },
|
||||
card: { padding: 20, paddingBottom: 5, borderRadius: 24, borderWidth: 1, marginBottom: 30 },
|
||||
criterioContainer: { marginBottom: 20 },
|
||||
criterioLabel: { fontSize: 14, fontWeight: '700', marginBottom: 12 },
|
||||
botoesContainer: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
botaoNota: { width: 48, height: 48, borderRadius: 14, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center' },
|
||||
botaoTexto: { fontSize: 18, fontWeight: '800' },
|
||||
notaCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 20, borderRadius: 24, borderWidth: 1, marginBottom: 30 },
|
||||
notaLabel: { fontSize: 16, fontWeight: '900' },
|
||||
gradeInput: { fontSize: 24, fontWeight: '900', borderWidth: 1.5, borderRadius: 16, width: 80, textAlign: 'center', paddingVertical: 12 },
|
||||
textArea: { borderRadius: 24, borderWidth: 1, padding: 20, minHeight: 120, fontSize: 15 },
|
||||
footerBtnContainer: { padding: 20, borderTopWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
|
||||
btnSubmit: { paddingVertical: 18, borderRadius: 20, alignItems: 'center', justifyContent: 'center', elevation: 3 },
|
||||
btnSubmitText: { color: '#FFF', fontSize: 17, fontWeight: '800' }
|
||||
});
|
||||
BIN
assets/images/logoepvc.png
Normal file
BIN
assets/images/logoepvc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/images/logoepvc2.png
Normal file
BIN
assets/images/logoepvc2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
28
package-lock.json
generated
28
package-lock.json
generated
@@ -16,9 +16,10 @@
|
||||
"@supabase/supabase-js": "^2.91.0",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"expo": "~54.0.27",
|
||||
"expo-asset": "~12.0.13",
|
||||
"expo-constants": "~18.0.11",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-file-system": "~19.0.22",
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
@@ -26,6 +27,7 @@
|
||||
"expo-linear-gradient": "~15.0.8",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-print": "~15.0.8",
|
||||
"expo-router": "~6.0.17",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.12",
|
||||
@@ -6487,13 +6489,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo-asset": {
|
||||
"version": "12.0.12",
|
||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
|
||||
"integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==",
|
||||
"version": "12.0.13",
|
||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz",
|
||||
"integrity": "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/image-utils": "^0.8.8",
|
||||
"expo-constants": "~18.0.12"
|
||||
"expo-constants": "~18.0.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
@@ -6525,9 +6527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo-file-system": {
|
||||
"version": "19.0.21",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
||||
"integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==",
|
||||
"version": "19.0.22",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.22.tgz",
|
||||
"integrity": "sha512-l9pgahSc7sJD0bP9vBNeXvZjy8QKDpVHVxWmei/ESQOrzmoj5BidziqLVsyZdxsi+PfdbTtttLTAmddH/JafYA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
@@ -6668,6 +6670,16 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-print": {
|
||||
"version": "15.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-print/-/expo-print-15.0.8.tgz",
|
||||
"integrity": "sha512-4O0Qzm0On5AmJIl9d+BT+ieTipFp658nHI4aX7vKEFPfj3dfQxG6rDJJpca+rrc9c4Ha8ZFYGvxJG5+4lFq2Pw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-router": {
|
||||
"version": "6.0.17",
|
||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.17.tgz",
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
"@supabase/supabase-js": "^2.91.0",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"expo": "~54.0.27",
|
||||
"expo-asset": "~12.0.13",
|
||||
"expo-constants": "~18.0.11",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-file-system": "~19.0.22",
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
@@ -29,6 +30,7 @@
|
||||
"expo-linear-gradient": "~15.0.8",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-print": "~15.0.8",
|
||||
"expo-router": "~6.0.17",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.12",
|
||||
|
||||
Reference in New Issue
Block a user