Files

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