338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
import { API_ENDPOINTS, buildApiUrl } from '@/assets/config/api';
|
|
import { useAuth } from '@/assets/contexts/useAuth';
|
|
import { cacheContacts, getCachedContacts } from '@/assets/services/offlineStorage';
|
|
import { colors } from '@/assets/styles/colors';
|
|
import { ContactData, ContactResponse, SocialMedia } from '@/assets/types';
|
|
import styles from '@/styles/screens/tabs/contactos.styles';
|
|
import { FontAwesome } from '@expo/vector-icons';
|
|
import * as Clipboard from 'expo-clipboard';
|
|
import { useEffect, useState } from 'react';
|
|
import { LoadingSpinner } from '@/assets/components/LoadingSpinner';
|
|
import {
|
|
Alert,
|
|
Image,
|
|
Linking,
|
|
Pressable,
|
|
RefreshControl,
|
|
ScrollView,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
const openPhone = (phone: string) => {
|
|
const clean = phone.replace(/[^\d+]/g, '');
|
|
Linking.openURL(`tel:${clean}`).catch(() =>
|
|
Alert.alert('Erro', 'Não foi possível abrir a aplicação de telefone.')
|
|
);
|
|
};
|
|
|
|
const openEmail = (email: string) => {
|
|
Linking.openURL(`mailto:${email}`).catch(() =>
|
|
Alert.alert('Erro', 'Não foi possível abrir a aplicação de e-mail.')
|
|
);
|
|
};
|
|
|
|
const openWhatsApp = (value: string) => {
|
|
const clean = value.replace(/[^\d+]/g, '');
|
|
const url = `https://wa.me/${clean}`;
|
|
Linking.openURL(url).catch(() =>
|
|
Alert.alert('Erro', 'Não foi possível abrir o WhatsApp.')
|
|
);
|
|
};
|
|
|
|
const openMaps = (address: string, lat?: number, lng?: number) => {
|
|
const url =
|
|
lat != null && lng != null
|
|
? `maps://?ll=${lat},${lng}&q=${encodeURIComponent(address)}`
|
|
: `maps:?q=${encodeURIComponent(address)}`;
|
|
|
|
Linking.openURL(url).catch(() =>
|
|
Linking.openURL(
|
|
lat != null && lng != null
|
|
? `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`
|
|
: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`
|
|
).catch(() => Alert.alert('Erro', 'Não foi possível abrir a aplicação de mapas.'))
|
|
);
|
|
};
|
|
|
|
const copyToClipboard = async (value: string, label: string) => {
|
|
await Clipboard.setStringAsync(value);
|
|
Alert.alert('Copiado', `${label} copiado para a área de transferência.`);
|
|
};
|
|
|
|
const extractPhone = (url: string): string => {
|
|
const match = url.match(/(?:phone=|wa\.me\/)([\d+]+)/);
|
|
if (match) return `+${match[1].replace(/^\+/, '')}`;
|
|
const clean = url.replace(/[^\d+]/g, '');
|
|
return clean.startsWith('+') ? clean : `+${clean}`;
|
|
};
|
|
|
|
const parseCoords = (raw?: string | null): { lat: number; lng: number } | null => {
|
|
if (!raw) return null;
|
|
const parts = raw.split(',').map((s) => parseFloat(s.trim()));
|
|
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
|
|
return { lat: parts[0], lng: parts[1] };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
|
|
const parseHorarios = (raw: string): { dia: string; horas: string }[] => {
|
|
return raw
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const sep = line.lastIndexOf(':');
|
|
if (sep > 0) {
|
|
return { dia: line.slice(0, sep).trim(), horas: line.slice(sep + 1).trim() };
|
|
}
|
|
const parts = line.split(/\s{2,}|\t/);
|
|
if (parts.length >= 2) {
|
|
return { dia: parts[0].trim(), horas: parts.slice(1).join(' ').trim() };
|
|
}
|
|
return { dia: line, horas: '' };
|
|
});
|
|
};
|
|
|
|
const SOCIAL_ICONS: Record<string, string> = {
|
|
facebook: 'facebook',
|
|
instagram: 'instagram',
|
|
tiktok: 'music', // FontAwesome doesn't have tiktok
|
|
twitter: 'twitter',
|
|
linkedin: 'linkedin',
|
|
youtube: 'youtube-play',
|
|
};
|
|
|
|
// ─── screen ───────────────────────────────────────────────────────────────────
|
|
|
|
export default function Contactos() {
|
|
const { token } = useAuth();
|
|
const [contactData, setContactData] = useState<ContactData | null>(null);
|
|
const [socials, setSocials] = useState<SocialMedia[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [mapLoading, setMapLoading] = useState(true);
|
|
const [mapError, setMapError] = useState(false);
|
|
|
|
const fetchContacts = async () => {
|
|
// 1. Mostrar cache imediatamente enquanto atualiza em fundo
|
|
const cached = await getCachedContacts();
|
|
if (cached.contactData) {
|
|
setContactData(cached.contactData);
|
|
setSocials(cached.socials);
|
|
setIsLoading(false);
|
|
}
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
if (token) formData.append('token', token);
|
|
|
|
const response = await fetch(buildApiUrl(API_ENDPOINTS.CONTACTS), {
|
|
method: 'POST',
|
|
headers: { Accept: 'application/json' },
|
|
body: formData,
|
|
});
|
|
|
|
const data: ContactResponse = await response.json();
|
|
|
|
console.log('data', data);
|
|
|
|
if (data.status === 200 && data.contact) {
|
|
const validSocials = (data.socials || []).filter((s) => s.value?.trim());
|
|
setContactData(data.contact);
|
|
setSocials(validSocials);
|
|
setError(null);
|
|
await cacheContacts(data.contact, validSocials);
|
|
} else if (!cached.contactData) {
|
|
setError(data.message || 'Não foi possível carregar os contactos.');
|
|
}
|
|
} catch {
|
|
if (!cached.contactData) {
|
|
setError('Sem ligação à internet e sem dados guardados.');
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchContacts();
|
|
}, [token]);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await fetchContacts();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={styles.centered}>
|
|
<LoadingSpinner size="large" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error && !contactData) {
|
|
return (
|
|
<View style={styles.centered}>
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const horarios = contactData?.horarios ? parseHorarios(contactData.horarios) : [];
|
|
const coords = parseCoords(contactData?.coordenadas);
|
|
const mapUrl = coords
|
|
? `https://staticmap.openstreetmap.de/staticmap.php?center=${coords.lat},${coords.lng}&zoom=15&size=600x300&markers=${coords.lat},${coords.lng},red-pushpin`
|
|
: null;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<ScrollView
|
|
contentContainerStyle={styles.content}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
|
|
|
|
<Text style={styles.pageTitle}>Contactos Agência</Text>
|
|
|
|
{/* ── Ações rápidas ── */}
|
|
<View style={styles.actionsRow}>
|
|
{!!contactData?.mobilePhone && (
|
|
<Pressable
|
|
style={styles.actionBtn}
|
|
onPress={() => openPhone(contactData.telephone)}>
|
|
<View style={styles.actionIconWrap}>
|
|
<Image source={require('@/assets/icons/telefone.png')} style={styles.actionIcon} />
|
|
</View>
|
|
<Text style={styles.actionLabel}>Ligar Agora</Text>
|
|
<Text style={styles.actionSub} numberOfLines={1}>
|
|
{contactData.telephone}
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
|
|
{!!contactData?.email && (
|
|
<Pressable
|
|
style={styles.actionBtn}
|
|
onPress={() => openEmail(contactData.email)}>
|
|
<View style={styles.actionIconWrap}>
|
|
<Image source={require('@/assets/icons/email.png')} style={styles.actionIcon} />
|
|
</View>
|
|
<Text style={styles.actionLabel}>Enviar Email</Text>
|
|
<Text style={styles.actionSub} numberOfLines={1}>
|
|
{contactData.email}
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
|
|
{!!contactData?.mobilePhone && (
|
|
<Pressable
|
|
style={styles.actionBtn}
|
|
onPress={() => openWhatsApp(contactData.mobilePhone!)}>
|
|
<View style={styles.actionIconWrap}>
|
|
<Image source={require('@/assets/icons/whatsapp.png')} style={styles.actionIcon} />
|
|
</View>
|
|
<Text style={styles.actionLabel}>Falar no Chat</Text>
|
|
<Text style={styles.actionSub} numberOfLines={1}>
|
|
{extractPhone(contactData.mobilePhone)}
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
|
|
{/* ── Localização ── */}
|
|
{!!contactData?.address && (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Localização</Text>
|
|
|
|
{!!mapUrl && (
|
|
<Pressable
|
|
onPress={() => openMaps(contactData.address, coords?.lat, coords?.lng)}
|
|
style={styles.mapImageWrap}>
|
|
<Image
|
|
source={{ uri: mapUrl }}
|
|
style={styles.mapImage}
|
|
resizeMode="cover"
|
|
onLoadStart={() => { setMapLoading(true); setMapError(false); }}
|
|
onLoad={() => setMapLoading(false)}
|
|
onError={() => { setMapLoading(false); setMapError(true); }}
|
|
/>
|
|
{mapLoading && !mapError && (
|
|
<View style={styles.mapOverlay}>
|
|
<LoadingSpinner size="small" />
|
|
</View>
|
|
)}
|
|
{mapError && (
|
|
<View style={styles.mapOverlay}>
|
|
<FontAwesome name="map-marker" size={28} color={colors.vermelho} />
|
|
<Text style={styles.mapErrorText}>Mapa indisponível</Text>
|
|
</View>
|
|
)}
|
|
</Pressable>
|
|
)}
|
|
|
|
<View style={styles.addressRow}>
|
|
<FontAwesome name="map-marker" size={16} color={colors.vermelho} />
|
|
<Text style={styles.addressText}>{contactData.address}</Text>
|
|
<Pressable onPress={() => copyToClipboard(contactData.address, 'Morada')}>
|
|
<FontAwesome name="copy" size={14} color={colors.azul} style={styles.copyIcon} />
|
|
</Pressable>
|
|
</View>
|
|
<Pressable
|
|
style={styles.direcoeBtn}
|
|
onPress={() => openMaps(contactData.address, coords?.lat, coords?.lng)}>
|
|
<Text style={styles.direcoeBtnText}>Obter Direções</Text>
|
|
<Image source={require('@/assets/icons/seta-up.png')} style={styles.saveButtonIcon} />
|
|
</Pressable>
|
|
</View>
|
|
)}
|
|
|
|
{/* ── Horário de Funcionamento ── */}
|
|
{horarios.length > 0 && (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Horário de Funcionamento</Text>
|
|
{horarios.map((item, idx) => (
|
|
<View
|
|
key={idx}
|
|
style={[styles.horarioRow, idx < horarios.length - 1 && styles.horarioRowBorder]}>
|
|
<Text style={styles.horarioDia}>{item.dia}</Text>
|
|
<Text style={[
|
|
styles.horarioHoras,
|
|
item.horas.toLowerCase() === 'fechado' && styles.horarioFechado,
|
|
]}>
|
|
{item.horas || '—'}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{/* ── Redes Sociais ── */}
|
|
{socials.length > 0 && (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Redes Sociais</Text>
|
|
<View style={styles.socialsRow}>
|
|
{socials.map((social, idx) => (
|
|
<Pressable
|
|
key={idx}
|
|
style={styles.socialBtn}
|
|
onPress={() => Linking.openURL(social.value).catch(() => null)}>
|
|
<FontAwesome
|
|
name={(SOCIAL_ICONS[social.key.toLowerCase()] ?? 'globe') as any}
|
|
size={20}
|
|
color={colors.azul}
|
|
/>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|