This commit is contained in:
2026-05-06 12:48:44 +01:00
parent 9097bfabbd
commit d6a73c3571
10 changed files with 1091 additions and 137 deletions

View File

@@ -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
}
}
}
}

View File

@@ -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 }
});

View File

@@ -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>
);
})
)}

View File

@@ -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 }
});

View 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}> 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' }
});

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/images/logoepvc2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

28
package-lock.json generated
View File

@@ -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",

View File

@@ -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",