atualizações

This commit is contained in:
2026-05-16 12:50:40 +01:00
parent e7ab95c2ea
commit 9835eab040
3 changed files with 1337 additions and 2430 deletions

View File

@@ -10,6 +10,7 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
Linking,
Modal,
Platform,
RefreshControl,
@@ -39,7 +40,7 @@ const getFeriadosMap = (ano: number) => ({
[`${ano}-01-01`]: "Ano Novo", [`${ano}-04-25`]: "Dia da Liberdade",
[`${ano}-05-01`]: "Dia do Trabalhador", [`${ano}-06-10`]: "Dia de Portugal",
[`${ano}-06-24`]: "São João (Vila do Conde)", [`${ano}-08-15`]: "Assunção de Nª Senhora",
[`${ano}-10-05`]: "Implantação da República", [`${ano}-11-01`]: "Todos os Santos",
[`${ano}-10-05`]: "Impl আমের da República", [`${ano}-11-01`]: "Todos os Santos",
[`${ano}-12-01`]: "Restauração da Independência", [`${ano}-12-08`]: "Imaculada Conceição",
[`${ano}-12-25`]: "Natal"
});
@@ -109,7 +110,7 @@ const AlunoHome = memo(() => {
const { data: eData } = await supabase
.from('estagios')
.select('id, data_inicio, data_fim, horas_diarias, empresas(nome, tutor_nome, tutor_telefone)')
.select('id, data_inicio, data_fim, horas_diarias, permite_sabados, permite_domingos, permite_feriados, empresas(nome, tutor_nome, tutor_telefone)')
.eq('aluno_id', user.id)
.order('data_fim', { ascending: false })
.limit(1)
@@ -164,13 +165,13 @@ const AlunoHome = memo(() => {
}
};
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, [selectedDate]));
useFocusEffect(useCallback(() => { fetchDadosSupabase(); }, []));
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchDadosSupabase(true);
setRefreshing(false);
}, [selectedDate]);
}, []);
const feriadosMap = useMemo(() => getFeriadosMap(new Date(selectedDate).getFullYear()), [selectedDate]);
@@ -182,17 +183,28 @@ const AlunoHome = memo(() => {
}, [estagioDetalhes, hojeStr]);
const infoData = useMemo(() => {
const dataObj = new Date(selectedDate);
const diaSemana = dataObj.getDay();
const nomeFeriado = feriadosMap[selectedDate];
const temEstagio = !!estagioDetalhes && estagioDetalhes.data_inicio && estagioDetalhes.data_fim;
const antesDoInicio = temEstagio && selectedDate < estagioDetalhes.data_inicio;
const depoisDoFim = temEstagio && selectedDate > estagioDetalhes.data_fim;
const estagioAtivo = statusEstagio === 'ativo';
const permiteSabados = estagioDetalhes?.permite_sabados === true;
const permiteDomingos = estagioDetalhes?.permite_domingos === true;
const permiteFeriados = estagioDetalhes?.permite_feriados === true;
let bloqueadoPorRegra = false;
if (diaSemana === 6 && !permiteSabados) bloqueadoPorRegra = true;
if (diaSemana === 0 && !permiteDomingos) bloqueadoPorRegra = true;
if (nomeFeriado && !permiteFeriados) bloqueadoPorRegra = true;
return {
valida: estagioAtivo && !antesDoInicio && !depoisDoFim && !nomeFeriado,
podeMarcar: estagioAtivo && selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !nomeFeriado,
valida: estagioAtivo && !antesDoInicio && !depoisDoFim && !bloqueadoPorRegra,
podeMarcar: estagioAtivo && selectedDate === hojeStr && !antesDoInicio && !depoisDoFim && !bloqueadoPorRegra,
nomeFeriado, antesDoInicio, depoisDoFim, foraDeRange: !temEstagio || antesDoInicio || depoisDoFim,
temEstagio, estagioAtivo
temEstagio, estagioAtivo, bloqueadoPorRegra
};
}, [selectedDate, estagioDetalhes, hojeStr, feriadosMap, statusEstagio]);
@@ -225,6 +237,7 @@ const AlunoHome = memo(() => {
const handlePresencaClick = async () => {
if (!infoData.temEstagio) return showAlert("Aguarde pela configuração do estágio.", "error");
if (!infoData.estagioAtivo) return showAlert("O estágio não está ativo neste momento.", "error");
if (infoData.bloqueadoPorRegra) return showAlert("Este dia não está autorizado no teu plano de estágio.", "error");
const { status } = await Location.getForegroundPermissionsAsync();
if (status === 'granted') {
@@ -297,16 +310,45 @@ const AlunoHome = memo(() => {
setEditandoSumario(false);
};
const fazerChamada = (telefone: string) => {
if (!telefone || telefone === "N/A") {
showAlert("Número de telefone indisponível.", "error");
return;
}
const numeroLimpo = telefone.replace(/[^0-9]/g, '');
Linking.openURL(`tel:${numeroLimpo}`).catch((err) => {
console.error(err);
showAlert("Não foi possível abrir o teclado do telefone.", "error");
});
};
const gerarMarcacoesCalendario = () => {
const marcacoes: any = {};
Object.keys(feriadosMap).forEach(d => { marcacoes[d] = { marked: true, dotColor: '#000000b7' }; });
const permiteSabados = estagioDetalhes?.permite_sabados === true;
const permiteDomingos = estagioDetalhes?.permite_domingos === true;
const permiteFeriados = estagioDetalhes?.permite_feriados === true;
Object.keys(feriadosMap).forEach(d => {
if (!permiteFeriados) {
marcacoes[d] = { marked: true, dotColor: '#000000b7' };
}
});
if (estagioDetalhes?.data_inicio) {
const start = new Date(estagioDetalhes.data_inicio);
const limit = new Date(hojeStr) < new Date(estagioDetalhes.data_fim) ? new Date(hojeStr) : new Date(estagioDetalhes.data_fim);
const limit = new Date(estagioDetalhes.data_fim);
for (let d = new Date(start); d <= limit; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
if (!feriadosMap[dateStr] && !registosDiarios[dateStr]) {
const day = d.getDay();
const isFeriado = !!feriadosMap[dateStr];
let bloqueado = false;
if (day === 6 && !permiteSabados) bloqueado = true;
if (day === 0 && !permiteDomingos) bloqueado = true;
if (isFeriado && !permiteFeriados) bloqueado = true;
if (!bloqueado && !registosDiarios[dateStr]) {
marcacoes[dateStr] = { marked: true, dotColor: '#0947f1b7' };
}
}
@@ -341,11 +383,41 @@ const AlunoHome = memo(() => {
const renderAvisoEstadoDia = () => {
const reg = registosDiarios[selectedDate];
let config = { icon: 'information-circle', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Sem Registo de Atividade' };
if (!reg) {
if (infoData.foraDeRange || !infoData.valida || selectedDate > hojeStr) return null;
} else if (reg.estado === 'presente') {
if (infoData.foraDeRange) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: themeStyles.card, padding: 16, borderRadius: 16, marginTop: 15, borderWidth: 1, borderColor: themeStyles.borda }}>
<Ionicons name="calendar-outline" size={28} color={themeStyles.textoSecundario} />
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: themeStyles.textoSecundario }}>Data fora do período de estágio</Text>
</View>
);
}
// 🟢 Correção: MOVIDO PARA CIMA para garantir que mostra em QUALQUER fim de semana ou feriado sem permissão
if (infoData.bloqueadoPorRegra) {
return (
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: themeStyles.card, padding: 16, borderRadius: 16, marginTop: 15, borderWidth: 1, borderColor: themeStyles.borda }}>
<Ionicons name="cafe-outline" size={28} color={themeStyles.textoSecundario} />
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: themeStyles.textoSecundario }}>Dia de Descanso / Não Útil</Text>
</View>
);
}
if (selectedDate > hojeStr) return null; // Apenas dias ÚTEIS no futuro ficam escondidos
let config = { icon: 'information-circle', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Nenhum Registo Efetuado' };
return (
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: config.bg, padding: 16, borderRadius: 16, marginTop: 15, borderWidth: 1, borderColor: config.cor + '40' }}>
<Ionicons name={config.icon as any} size={28} color={config.cor} />
<Text style={{ flex: 1, marginLeft: 12, fontWeight: '800', fontSize: 14, color: config.cor }}>{config.texto}</Text>
</View>
);
}
let config = { icon: 'information-circle', cor: themeStyles.azul, bg: themeStyles.azulSuave, texto: 'Sem Registo de Atividade' };
if (reg.estado === 'presente') {
const temSumario = reg.sumario && reg.sumario.trim() !== '';
if (reg.estado_tutor === 'aprovado' && temSumario) config = { icon: 'checkmark-circle', cor: themeStyles.verde, bg: themeStyles.verde + '20', texto: 'Presença Confirmada e Validada' };
else if (!temSumario) config = { icon: 'warning', cor: themeStyles.amarelo, bg: themeStyles.amarelo + '20', texto: 'Presença Requer Submissão de Atividades' };
@@ -426,13 +498,8 @@ const AlunoHome = memo(() => {
{!isLoadingDB && (
estagioDetalhes ? (
// ========================================================
// CAIXA MESTRA DO CONTEÚDO DA TAB
// Adicionado minHeight: 160 e justifyContent para fixar tamanho!
// ========================================================
<View style={[styles.dashboardCard, { backgroundColor: themeStyles.card, borderColor: themeStyles.borda, borderLeftColor: userRole !== 'aluno' ? (statusEstagio === 'concluido' ? '#94A3B8' : themeStyles.azul) : undefined }]}>
{/* O Cabeçalho (Empresa) só aparece para Professores e Entidades */}
{userRole !== 'aluno' && (
<>
<View style={styles.dashHeader}>
@@ -448,9 +515,7 @@ const AlunoHome = memo(() => {
</>
)}
{/* CONTEÚDO DAS TABS COM ALTURA FIXA */}
<View style={{ minHeight: 110, justifyContent: 'center' }}>
{/* ABA: ASSIDUIDADE */}
{activeTab === 'assiduidade' && (
<View>
{userRole === 'aluno' && (
@@ -477,7 +542,6 @@ const AlunoHome = memo(() => {
</View>
)}
{/* ABA: HORÁRIO */}
{activeTab === 'horario' && (
<View style={{ alignItems: 'center' }}>
<Ionicons name="time" size={32} color={themeStyles.laranja} style={{ marginBottom: 5 }} />
@@ -505,7 +569,6 @@ const AlunoHome = memo(() => {
</View>
)}
{/* ABA: DETALHES DA ENTIDADE */}
{activeTab === 'info' && (
<View>
<View style={styles.infoRow}>
@@ -518,10 +581,21 @@ const AlunoHome = memo(() => {
<View style={styles.infoRow}>
<Ionicons name="call" size={18} color={themeStyles.verde} />
<View style={{ marginLeft: 10 }}>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Contacto Oficial</Text>
<Text style={[styles.infoValue, { color: themeStyles.texto }]}>{estagioDetalhes.empresas?.tutor_telefone || "N/A"}</Text>
</View>
<TouchableOpacity
style={{ marginLeft: 10, flex: 1, flexDirection: 'row', alignItems: 'center' }}
onPress={() => fazerChamada(estagioDetalhes.empresas?.tutor_telefone)}
activeOpacity={0.7}
>
<View>
<Text style={[styles.infoLabel, { color: themeStyles.textoSecundario }]}>Contacto Oficial</Text>
<Text style={[styles.infoValue, { color: themeStyles.azul, textDecorationLine: 'underline' }]}>
{estagioDetalhes.empresas?.tutor_telefone || "N/A"}
</Text>
</View>
{estagioDetalhes.empresas?.tutor_telefone && estagioDetalhes.empresas.tutor_telefone !== 'N/A' && (
<Ionicons name="open-outline" size={14} color={themeStyles.azul} style={{ marginLeft: 6, marginTop: 12 }} />
)}
</TouchableOpacity>
</View>
<View style={[styles.dashDividerHorizontal, { backgroundColor: themeStyles.borda }]} />

View File

@@ -9,9 +9,10 @@ import {
ScrollView,
StatusBar,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
TouchableOpacity, // 🟢 NOVO: Importado para os botões de regras
View
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -29,6 +30,9 @@ interface Estagio {
data_fim: string;
horas_diarias?: string;
horas_totais?: number;
permite_sabados?: boolean; // 🟢 NOVO
permite_domingos?: boolean; // 🟢 NOVO
permite_feriados?: boolean; // 🟢 NOVO
alunos: { nome: string; turma_curso: string; ano: number };
empresas: { id: string; nome: string; morada: string; tutor_nome: string; tutor_telefone: string; curso: string };
}
@@ -107,6 +111,11 @@ export default function Estagios() {
const [hTardeIni, setHTardeIni] = useState('');
const [hTardeFim, setHTardeFim] = useState('');
// 🟢 NOVO: Estados para as Regras de Assiduidade
const [permiteSabados, setPermiteSabados] = useState(false);
const [permiteDomingos, setPermiteDomingos] = useState(false);
const [permiteFeriados, setPermiteFeriados] = useState(false);
const showToast = useCallback((message: string, type: 'error' | 'success' | 'info' = 'info') => {
setToast({ visible: true, message, type });
Animated.timing(slideAnim, { toValue: insets.top + 10, duration: 300, useNativeDriver: true }).start(() => {
@@ -208,7 +217,7 @@ export default function Estagios() {
return `${h.padStart(2, '0')}:${m.padStart(2, '0')}`;
};
// --- LÓGICA MANTIDA INTACTA ---
// 🟢 NOVO: A magia agora respeita as regras de assiduidade!
const calcularMagia = (tipo: 'dataFim' | 'horasTotais') => {
if (!dataInicio || dataInicio.length < 10) return showCustomAlert("Atenção", "Preenche a Data de Início completa primeiro!");
if (!hManhaIni && !hTardeIni) return showCustomAlert("Atenção", "Preenche primeiro o horário diário (Manhã e/ou Tarde)!");
@@ -232,7 +241,13 @@ export default function Estagios() {
const dateStr = currentDate.toISOString().split('T')[0];
const feriadosMapAno = getFeriadosMap(currentDate.getFullYear());
if (day !== 0 && day !== 6 && !feriadosMapAno[dateStr]) {
let diaValidoParaContar = true;
if (day === 6 && !permiteSabados) diaValidoParaContar = false;
if (day === 0 && !permiteDomingos) diaValidoParaContar = false;
if (feriadosMapAno[dateStr] && !permiteFeriados) diaValidoParaContar = false;
if (diaValidoParaContar) {
diasContados++;
}
if (diasContados < diasNecessarios) {
@@ -255,7 +270,13 @@ export default function Estagios() {
const dateStr = currentDate.toISOString().split('T')[0];
const feriadosMapAno = getFeriadosMap(currentDate.getFullYear());
if (day !== 0 && day !== 6 && !feriadosMapAno[dateStr]) {
let diaValidoParaContar = true;
if (day === 6 && !permiteSabados) diaValidoParaContar = false;
if (day === 0 && !permiteDomingos) diaValidoParaContar = false;
if (feriadosMapAno[dateStr] && !permiteFeriados) diaValidoParaContar = false;
if (diaValidoParaContar) {
diasUteis++;
}
currentDate.setDate(currentDate.getDate() + 1);
@@ -303,6 +324,7 @@ export default function Estagios() {
}).eq('id', empresaSelecionada.id);
}
// 🟢 NOVO: Payload atualizado com as permissões
const payload = {
aluno_id: alunoSelecionado?.id,
empresa_id: empresaSelecionada?.id,
@@ -312,6 +334,9 @@ export default function Estagios() {
horas_diarias: totalHorasDiarias,
horas_totais: totais,
estado: 'Ativo',
permite_sabados: permiteSabados,
permite_domingos: permiteDomingos,
permite_feriados: permiteFeriados
};
const { data: estData, error: errE } = editandoEstagio
@@ -359,6 +384,8 @@ export default function Estagios() {
setModalVisible(false); setEditandoEstagio(null); setAlunoSelecionado(null); setEmpresaSelecionada(null);
setDataInicio(''); setDataFim(''); setHorasTotaisEstagio(''); setHManhaIni(''); setHManhaFim(''); setHTardeIni(''); setHTardeFim('');
setPesquisaAluno(''); setPesquisaEmpresa('');
// 🟢 Resetar os switches
setPermiteSabados(false); setPermiteDomingos(false); setPermiteFeriados(false);
setPasso(1);
};
@@ -454,6 +481,12 @@ export default function Estagios() {
setDataInicio(e.data_inicio || '');
setDataFim(e.data_fim || '');
setHorasTotaisEstagio(e.horas_totais?.toString() || '');
// 🟢 NOVO: Preencher as permissões no formulário se estivermos a editar
setPermiteSabados(e.permite_sabados || false);
setPermiteDomingos(e.permite_domingos || false);
setPermiteFeriados(e.permite_feriados || false);
carregarHorariosEdicao(e.id);
setPasso(2); setModalVisible(true);
}}
@@ -657,6 +690,44 @@ export default function Estagios() {
</View>
</View>
{/* 🟢 NOVO GRUPO: REGRAS DE ASSIDUIDADE */}
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<Text style={[styles.groupTitle, { color: cores.azul, marginBottom: 8 }]}>Regras de Assiduidade</Text>
<Text style={{ fontSize: 11, color: cores.secundario, marginBottom: 15 }}>
Selecione abaixo os dias excepcionais em que este aluno está autorizado a estagiar e a marcar presença no sistema.
</Text>
<View style={styles.switchRow}>
<Text style={[styles.switchLabel, { color: cores.texto }]}>Permitir registos ao Sábado</Text>
<Switch
value={permiteSabados}
onValueChange={setPermiteSabados}
trackColor={{ false: cores.borda, true: cores.azul }}
thumbColor="#FFF"
/>
</View>
<View style={styles.switchRow}>
<Text style={[styles.switchLabel, { color: cores.texto }]}>Permitir registos ao Domingo</Text>
<Switch
value={permiteDomingos}
onValueChange={setPermiteDomingos}
trackColor={{ false: cores.borda, true: cores.azul }}
thumbColor="#FFF"
/>
</View>
<View style={[styles.switchRow, { borderBottomWidth: 0 }]}>
<Text style={[styles.switchLabel, { color: cores.texto }]}>Permitir registos em Feriados</Text>
<Switch
value={permiteFeriados}
onValueChange={setPermiteFeriados}
trackColor={{ false: cores.borda, true: cores.azul }}
thumbColor="#FFF"
/>
</View>
</View>
<View style={[styles.modernGroup, { backgroundColor: cores.fundo, borderColor: cores.borda }]}>
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text style={[styles.groupTitle, { color: cores.azul }]}>Horário de Trabalho</Text>
@@ -880,6 +951,10 @@ const styles = StyleSheet.create({
totalBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
totalText: { color: '#fff', fontSize: 11, fontWeight: '900' },
// 🟢 NOVO: Estilos para as Switches
switchRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderColor: 'rgba(0,0,0,0.05)' },
switchLabel: { fontSize: 13, fontWeight: '700' },
modalFooter: { flexDirection: 'row', gap: 15, marginTop: 15 },
btnModalPri: { flex: 2, height: 60, borderRadius: 18, justifyContent: 'center', alignItems: 'center' },

3558
package-lock.json generated

File diff suppressed because it is too large Load Diff