atualizacoes
This commit is contained in:
@@ -2,16 +2,18 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as Location from 'expo-location'; // <-- Adicionado
|
||||
import { useRouter } from 'expo-router';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View
|
||||
ActivityIndicator, Alert, Platform, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, View
|
||||
} from 'react-native';
|
||||
import { Calendar, LocaleConfig } from 'react-native-calendars';
|
||||
import { useTheme } from '../../themecontext';
|
||||
import { supabase } from '../lib/supabase'; // <-- Garante que tens este arquivo configurado
|
||||
|
||||
// Configuração PT
|
||||
// Configuração PT (Mantida)
|
||||
LocaleConfig.locales['pt'] = {
|
||||
monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
|
||||
monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'],
|
||||
@@ -34,23 +36,6 @@ const getFeriadosMap = (ano: number) => {
|
||||
[`${ano}-12-08`]: "Imaculada Conceição",
|
||||
[`${ano}-12-25`]: "Natal"
|
||||
};
|
||||
|
||||
const a = ano % 19, b = Math.floor(ano / 100), c = ano % 100;
|
||||
const d = Math.floor(b / 4), e = b % 4, f_calc = Math.floor((b + 8) / 25);
|
||||
const g = Math.floor((b - f_calc + 1) / 3), h = (19 * a + b - d - g + 15) % 30;
|
||||
const i = Math.floor(c / 4), k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
const mes = Math.floor((h + l - 7 * m + 114) / 31);
|
||||
const dia = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
|
||||
const pascoa = new Date(ano, mes - 1, dia);
|
||||
const formatar = (dt: Date) => dt.toISOString().split('T')[0];
|
||||
|
||||
f[formatar(pascoa)] = "Páscoa";
|
||||
f[formatar(new Date(pascoa.getTime() - 47 * 24 * 3600 * 1000))] = "Carnaval";
|
||||
f[formatar(new Date(pascoa.getTime() - 2 * 24 * 3600 * 1000))] = "Sexta-feira Santa";
|
||||
f[formatar(new Date(pascoa.getTime() + 60 * 24 * 3600 * 1000))] = "Corpo de Deus";
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
@@ -67,6 +52,7 @@ const AlunoHome = memo(() => {
|
||||
const [faltasJustificadas, setFaltasJustificadas] = useState<Record<string, any>>({});
|
||||
const [pdf, setPdf] = useState<any>(null);
|
||||
const [editandoSumario, setEditandoSumario] = useState(false);
|
||||
const [isLocating, setIsLocating] = useState(false); // Novo estado para loading
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
@@ -106,15 +92,12 @@ const AlunoHome = memo(() => {
|
||||
const diaSemana = data.getDay();
|
||||
const ehFimDeSemana = diaSemana === 0 || diaSemana === 6;
|
||||
const foraDoIntervalo = selectedDate < configEstagio.inicio || selectedDate > configEstagio.fim;
|
||||
|
||||
// CORREÇÃO: Verifica se o dia selecionado é exatamente HOJE
|
||||
const ehHoje = selectedDate === hojeStr;
|
||||
const ehFuturo = selectedDate > hojeStr;
|
||||
const nomeFeriado = feriadosMap[selectedDate];
|
||||
|
||||
return {
|
||||
valida: !ehFimDeSemana && !foraDoIntervalo && !nomeFeriado,
|
||||
// Só permite presença se for o dia atual
|
||||
podeMarcarPresenca: ehHoje && !foraDoIntervalo && !nomeFeriado,
|
||||
ehFuturo,
|
||||
nomeFeriado
|
||||
@@ -135,21 +118,78 @@ const AlunoHome = memo(() => {
|
||||
return marcacoes;
|
||||
}, [presencas, faltas, sumarios, faltasJustificadas, selectedDate, listaFeriados]);
|
||||
|
||||
// --- LOGICA DE PRESENÇA COM LOCALIZAÇÃO ---
|
||||
const handlePresenca = async () => {
|
||||
if (!infoData.podeMarcarPresenca) return Alert.alert("Bloqueado", "A presença só pode ser marcada no próprio dia.");
|
||||
const novas = { ...presencas, [selectedDate]: true };
|
||||
setPresencas(novas);
|
||||
await AsyncStorage.setItem('@presencas', JSON.stringify(novas));
|
||||
|
||||
setIsLocating(true);
|
||||
try {
|
||||
// 1. Pedir Permissão
|
||||
let { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
Alert.alert('Erro', 'Precisas de aceitar a localização para marcar presença.');
|
||||
setIsLocating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Obter Localização exata
|
||||
let location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
|
||||
const { latitude, longitude } = location.coords;
|
||||
|
||||
// 3. Salvar no Supabase (Para o Professor ver)
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error("Usuário não autenticado.");
|
||||
|
||||
const { error } = await supabase.from('presencas').upsert({
|
||||
aluno_id: user.id,
|
||||
data: selectedDate,
|
||||
estado: 'presente',
|
||||
lat: latitude,
|
||||
lng: longitude
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// 4. Salvar Localmente (Como estava no teu código)
|
||||
const novas = { ...presencas, [selectedDate]: true };
|
||||
setPresencas(novas);
|
||||
await AsyncStorage.setItem('@presencas', JSON.stringify(novas));
|
||||
|
||||
Alert.alert("Sucesso", "Presença marcada com localização!");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Alert.alert("Erro", "Não foi possível salvar a presença. Se estiveres sem net, vai dar merda.");
|
||||
} finally {
|
||||
setIsLocating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFalta = async () => {
|
||||
if (!infoData.valida) return Alert.alert("Bloqueado", "Data inválida.");
|
||||
|
||||
// Para que o professor veja a falta, também temos de mandar para o Supabase
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await supabase.from('presencas').upsert({
|
||||
aluno_id: user.id,
|
||||
data: selectedDate,
|
||||
estado: 'faltou'
|
||||
});
|
||||
}
|
||||
|
||||
const novas = { ...faltas, [selectedDate]: true };
|
||||
setFaltas(novas);
|
||||
await AsyncStorage.setItem('@faltas', JSON.stringify(novas));
|
||||
};
|
||||
|
||||
const guardarSumario = async () => {
|
||||
// Enviar sumário para o Supabase também
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await supabase.from('presencas').update({ sumario: sumarios[selectedDate] })
|
||||
.match({ aluno_id: user.id, data: selectedDate });
|
||||
}
|
||||
|
||||
await AsyncStorage.setItem('@sumarios', JSON.stringify(sumarios));
|
||||
setEditandoSumario(false);
|
||||
Alert.alert("Sucesso", "Sumário guardado!");
|
||||
@@ -190,9 +230,9 @@ const AlunoHome = memo(() => {
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, styles.btnPresenca, (!infoData.podeMarcarPresenca || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
onPress={handlePresenca}
|
||||
disabled={!infoData.podeMarcarPresenca || !!presencas[selectedDate] || !!faltas[selectedDate]}
|
||||
disabled={!infoData.podeMarcarPresenca || !!presencas[selectedDate] || !!faltas[selectedDate] || isLocating}
|
||||
>
|
||||
<Text style={styles.txtBtn}>Marcar Presença</Text>
|
||||
{isLocating ? <ActivityIndicator color="#fff" /> : <Text style={styles.txtBtn}>Marcar Presença</Text>}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, styles.btnFalta, (!infoData.valida || presencas[selectedDate] || faltas[selectedDate]) && styles.disabled]}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Linking,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
@@ -13,248 +14,205 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../../../themecontext';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
interface Presenca {
|
||||
id: string;
|
||||
data: string;
|
||||
estado: 'presente' | 'faltou';
|
||||
localizacao?: { lat: number; lng: number };
|
||||
hora?: string; // Adicionei hora para o design ficar mais rico
|
||||
estado: string;
|
||||
sumario: string | null;
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
}
|
||||
|
||||
/* DADOS EXEMPLO */
|
||||
const presencasData: Presenca[] = [
|
||||
{
|
||||
data: '2024-01-10',
|
||||
hora: '09:00',
|
||||
estado: 'presente',
|
||||
localizacao: { lat: 41.55, lng: -8.42 },
|
||||
},
|
||||
{
|
||||
data: '2024-01-11',
|
||||
hora: '09:15',
|
||||
estado: 'faltou'
|
||||
},
|
||||
];
|
||||
|
||||
export default function CalendarioPresencas() {
|
||||
export default function HistoricoPresencas() {
|
||||
const router = useRouter();
|
||||
const { nome } = useLocalSearchParams<{ nome: string }>();
|
||||
const { alunoId, nome } = useLocalSearchParams();
|
||||
const { isDarkMode } = useTheme();
|
||||
const [presencas, setPresencas] = useState<Presenca[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const cores = useMemo(
|
||||
() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
verde: '#10B981',
|
||||
verdeSuave: 'rgba(16, 185, 129, 0.1)',
|
||||
vermelho: '#EF4444',
|
||||
vermelhoSuave: 'rgba(239, 68, 68, 0.1)',
|
||||
azul: '#3B82F6',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
}),
|
||||
[isDarkMode]
|
||||
);
|
||||
const cores = useMemo(() => ({
|
||||
fundo: isDarkMode ? '#0F0F0F' : '#F8FAFC',
|
||||
card: isDarkMode ? '#1A1A1A' : '#FFFFFF',
|
||||
texto: isDarkMode ? '#F8FAFC' : '#1E293B',
|
||||
secundario: isDarkMode ? '#94A3B8' : '#64748B',
|
||||
azul: '#3B82F6',
|
||||
azulSuave: isDarkMode ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borda: isDarkMode ? '#2D2D2D' : '#E2E8F0',
|
||||
verde: '#10B981',
|
||||
vermelho: '#EF4444',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const abrirMapa = (lat: number, lng: number) => {
|
||||
const url = Platform.OS === 'ios'
|
||||
? `maps://app?saddr=&daddr=${lat},${lng}`
|
||||
: `google.navigation:q=${lat},${lng}`;
|
||||
Linking.openURL(url);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (alunoId) fetchHistorico();
|
||||
}, [alunoId]);
|
||||
|
||||
async function fetchHistorico() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from('presencas')
|
||||
.select('*')
|
||||
.eq('aluno_id', alunoId)
|
||||
.order('data', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setPresencas(data || []);
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
Alert.alert("Erro", "Falha ao carregar o histórico real.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: cores.fundo }]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
|
||||
{/* StatusBar configurada para não sobrepor o conteúdo */}
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
translucent
|
||||
backgroundColor="transparent"
|
||||
/>
|
||||
|
||||
{/* HEADER SUPERIOR */}
|
||||
<View style={[styles.headerFixed, { borderBottomColor: cores.borda }]}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="chevron-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerInfo}>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>{nome}</Text>
|
||||
<Text style={[styles.subtitle, { color: cores.secundario }]}>Histórico de Registos</Text>
|
||||
{/* Header com design idêntico ao anterior */}
|
||||
<View style={styles.headerFixed}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="arrow-back" size={24} color={cores.texto} />
|
||||
</TouchableOpacity>
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text style={[styles.title, { color: cores.texto }]}>Histórico</Text>
|
||||
<Text style={[styles.subtitle, { color: cores.secundario }]}>{nome}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={fetchHistorico} style={styles.refreshBtn}>
|
||||
<Ionicons name="refresh" size={22} color={cores.azul} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* RESUMO RÁPIDO */}
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={[styles.statBox, { backgroundColor: cores.card }]}>
|
||||
<Text style={[styles.statLabel, { color: cores.secundario }]}>PRESENÇAS</Text>
|
||||
<Text style={[styles.statValue, { color: cores.verde }]}>12</Text>
|
||||
</View>
|
||||
<View style={[styles.statBox, { backgroundColor: cores.card }]}>
|
||||
<Text style={[styles.statLabel, { color: cores.secundario }]}>FALTAS</Text>
|
||||
<Text style={[styles.statValue, { color: cores.vermelho }]}>2</Text>
|
||||
</View>
|
||||
{loading ? (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={cores.azul} />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: cores.texto }]}>Atividade Recente</Text>
|
||||
|
||||
{presencasData.map((p, i) => {
|
||||
const isPresente = p.estado === 'presente';
|
||||
|
||||
return (
|
||||
<View key={i} style={styles.timelineItem}>
|
||||
{/* LINHA LATERAL (TIMELINE) */}
|
||||
<View style={styles.timelineLeft}>
|
||||
<View style={[styles.dot, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
|
||||
{i !== presencasData.length - 1 && <View style={[styles.line, { backgroundColor: cores.borda }]} />}
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View>
|
||||
<Text style={[styles.dateText, { color: cores.texto }]}>
|
||||
{new Date(p.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long' })}
|
||||
</Text>
|
||||
<Text style={[styles.hourText, { color: cores.secundario }]}>
|
||||
Registado às {p.hora || '--:--'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: isPresente ? cores.verdeSuave : cores.vermelhoSuave }]}>
|
||||
<Text style={[styles.statusText, { color: isPresente ? cores.verde : cores.vermelho }]}>
|
||||
{p.estado.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.cardDivider, { backgroundColor: cores.borda }]} />
|
||||
|
||||
<View style={styles.cardAction}>
|
||||
{isPresente ? (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => abrirMapa(p.localizacao!.lat, p.localizacao!.lng)}
|
||||
>
|
||||
<Ionicons name="location-outline" size={18} color={cores.azul} />
|
||||
<Text style={[styles.actionText, { color: cores.azul }]}>Ver Localização</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => router.push('/Professor/Alunos/Faltas')}
|
||||
>
|
||||
<Ionicons name="alert-circle-outline" size={18} color={cores.vermelho} />
|
||||
<Text style={[styles.actionText, { color: cores.vermelho }]}>Justificar Falta</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{presencas.length === 0 ? (
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="calendar-outline" size={48} color={cores.secundario} style={{ opacity: 0.5 }} />
|
||||
<Text style={[styles.emptyText, { color: cores.secundario }]}>
|
||||
Sem registos reais para este aluno.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
) : (
|
||||
presencas.map((item) => {
|
||||
const isPresente = item.estado === 'presente';
|
||||
|
||||
return (
|
||||
<View key={item.id} style={[styles.card, { backgroundColor: cores.card, borderColor: cores.borda }]}>
|
||||
<View style={styles.cardHeader}>
|
||||
{/* Ícone de Calendário no lugar do Avatar para manter o peso visual */}
|
||||
<View style={[styles.iconBox, { backgroundColor: cores.azulSuave }]}>
|
||||
<Ionicons name="calendar" size={20} color={cores.azul} />
|
||||
</View>
|
||||
|
||||
<View style={styles.info}>
|
||||
<Text style={[styles.dateText, { color: cores.texto }]}>
|
||||
{new Date(item.data).toLocaleDateString('pt-PT', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</Text>
|
||||
<View style={styles.statusBadge}>
|
||||
<View style={[styles.dot, { backgroundColor: isPresente ? cores.verde : cores.vermelho }]} />
|
||||
<Text style={[styles.subText, { color: cores.secundario }]}>
|
||||
{item.estado.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.lat && (
|
||||
<Ionicons name="location-sharp" size={18} color={cores.azul} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Área do Sumário com o estilo de "nota" do ecrã anterior */}
|
||||
<View style={[styles.sumarioBox, { backgroundColor: isDarkMode ? 'rgba(255,255,255,0.03)' : '#F1F5F9' }]}>
|
||||
<Text style={[styles.sumarioLabel, { color: cores.secundario }]}>Sumário do Dia</Text>
|
||||
<Text style={[styles.sumarioText, { color: cores.texto }]}>
|
||||
{item.sumario || "O aluno ainda não preencheu o sumário deste dia."}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
safe: {
|
||||
flex: 1,
|
||||
// Garante que o conteúdo não fica debaixo da barra de notificações (Android)
|
||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0
|
||||
},
|
||||
headerFixed: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 15,
|
||||
borderBottomWidth: 1,
|
||||
marginTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
|
||||
headerFixed: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 10
|
||||
},
|
||||
backBtn: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: 70
|
||||
},
|
||||
headerInfo: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: { fontSize: 18, fontWeight: '800' },
|
||||
subtitle: { fontSize: 12, fontWeight: '500' },
|
||||
scrollContainer: { padding: 20 },
|
||||
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 30,
|
||||
},
|
||||
statBox: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
borderRadius: 20,
|
||||
alignItems: 'center',
|
||||
backBtn: { width: 40, height: 40, justifyContent: 'center' },
|
||||
refreshBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'flex-end' },
|
||||
title: { fontSize: 22, fontWeight: '800' },
|
||||
subtitle: { fontSize: 13, marginTop: -2, fontWeight: '600' },
|
||||
scrollContent: { padding: 20 },
|
||||
card: {
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
marginBottom: 15,
|
||||
borderWidth: 1,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
shadowRadius: 5
|
||||
},
|
||||
statLabel: { fontSize: 9, fontWeight: '800', letterSpacing: 1 },
|
||||
statValue: { fontSize: 22, fontWeight: '900', marginTop: 4 },
|
||||
|
||||
sectionTitle: { fontSize: 16, fontWeight: '800', marginBottom: 20 },
|
||||
|
||||
timelineItem: {
|
||||
flexDirection: 'row',
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center' },
|
||||
iconBox: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
timelineLeft: {
|
||||
width: 30,
|
||||
alignItems: 'center',
|
||||
info: { flex: 1, marginLeft: 15 },
|
||||
dateText: { fontSize: 16, fontWeight: '700' },
|
||||
statusBadge: { flexDirection: 'row', alignItems: 'center', marginTop: 4 },
|
||||
dot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 },
|
||||
subText: { fontSize: 11, fontWeight: '800', letterSpacing: 0.5 },
|
||||
sumarioBox: {
|
||||
marginTop: 15,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
},
|
||||
dot: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
zIndex: 2,
|
||||
sumarioLabel: {
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 5,
|
||||
letterSpacing: 0.5
|
||||
},
|
||||
line: {
|
||||
width: 2,
|
||||
flex: 1,
|
||||
marginTop: -5,
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
borderWidth: 1,
|
||||
marginLeft: 10,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 8,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
dateText: { fontSize: 15, fontWeight: '700' },
|
||||
hourText: { fontSize: 12, marginTop: 2 },
|
||||
statusBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
statusText: { fontSize: 9, fontWeight: '900' },
|
||||
cardDivider: {
|
||||
height: 1,
|
||||
marginVertical: 12,
|
||||
},
|
||||
cardAction: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
actionButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
actionText: { fontSize: 13, fontWeight: '700' },
|
||||
sumarioText: { fontSize: 13, lineHeight: 18, opacity: 0.9 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
empty: { alignItems: 'center', marginTop: 60 },
|
||||
emptyText: { marginTop: 15, fontSize: 14, fontWeight: '600', textAlign: 'center' },
|
||||
});
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-router": "~6.0.17",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.12",
|
||||
@@ -6576,6 +6577,15 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-location": {
|
||||
"version": "19.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-19.0.8.tgz",
|
||||
"integrity": "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-modules-autolinking": {
|
||||
"version": "3.0.24",
|
||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-location": "~19.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