import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, FlatList, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import MapView, { Marker, UrlTile, PROVIDER_DEFAULT } from 'react-native-maps'; import { Contact } from '@/store/useContactStore'; import { getAllContactsForMap } from '@/lib/database'; // ── Status colour map ──────────────────────────────────────────────────────── const STATUS_COLORS: Record = { Active: '#1A6B72', 'Return Visit': '#E65100', 'Bible Study': '#2E7D32', 'Not Interested': '#6B7280', 'Do Not Call': '#C62828', }; const DEFAULT_PIN_COLOR = '#1A6B72'; // ── Cluster helpers (manual — no external package required) ────────────────── const CLUSTER_THRESHOLD = 0.0005; // ~55 m interface ClusterGroup { key: string; latitude: number; longitude: number; contacts: Contact[]; } function clusterContacts(contacts: Contact[]): ClusterGroup[] { const groups: ClusterGroup[] = []; const assigned = new Set(); for (const c of contacts) { if (assigned.has(c.id) || c.latitude == null || c.longitude == null) continue; const group: Contact[] = [c]; assigned.add(c.id); for (const other of contacts) { if (assigned.has(other.id) || other.latitude == null || other.longitude == null) continue; const dLat = Math.abs((c.latitude ?? 0) - (other.latitude ?? 0)); const dLng = Math.abs((c.longitude ?? 0) - (other.longitude ?? 0)); if (dLat < CLUSTER_THRESHOLD && dLng < CLUSTER_THRESHOLD) { group.push(other); assigned.add(other.id); } } groups.push({ key: c.id, latitude: c.latitude ?? 0, longitude: c.longitude ?? 0, contacts: group, }); } return groups; } // ── Mini contact card ──────────────────────────────────────────────────────── interface MiniCardProps { contact: Contact; onClose: () => void; onViewFull: (id: string) => void; } function MiniCard({ contact, onClose, onViewFull }: MiniCardProps) { const pinColor = STATUS_COLORS[contact.status] ?? DEFAULT_PIN_COLOR; return ( {contact.fullName} {contact.territoryCode ? ( Territory: {contact.territoryCode} ) : null} Last visit: — {contact.status} onViewFull(contact.id)} > View Full Record → ); } // ── No-GPS row ─────────────────────────────────────────────────────────────── interface NoGpsRowProps { contact: Contact; onPress: (id: string) => void; } function NoGpsRow({ contact, onPress }: NoGpsRowProps) { const initials = contact.fullName .split(' ') .map((w) => w[0]) .join('') .toUpperCase() .slice(0, 2); const pinColor = STATUS_COLORS[contact.status] ?? DEFAULT_PIN_COLOR; return ( onPress(contact.id)}> {initials} {contact.fullName} {contact.status} {contact.territoryCode ? ` — ${contact.territoryCode}` : ''} ); } // ── Main Screen ────────────────────────────────────────────────────────────── export default function MapScreen() { const router = useRouter(); const mapRef = useRef(null); const [contacts, setContacts] = useState([]); const [loading, setLoading] = useState(true); const [territories, setTerritories] = useState([]); const [selectedTerritory, setSelectedTerritory] = useState('All'); const [selectedContact, setSelectedContact] = useState(null); // Load on mount useEffect(() => { (async () => { try { const all = await getAllContactsForMap(); setContacts(all); // Unique territory codes from GPS contacts const codes = Array.from( new Set( all .filter((c) => c.latitude != null && c.longitude != null && c.territoryCode) .map((c) => c.territoryCode as string) ) ).sort(); setTerritories(codes); } catch (e) { console.error('Map load error', e); } finally { setLoading(false); } })(); }, []); // Derived lists const gpsContacts = contacts.filter((c) => c.latitude != null && c.longitude != null); const noGpsContacts = contacts.filter((c) => c.latitude == null || c.longitude == null); const filteredGps = selectedTerritory === 'All' ? gpsContacts : gpsContacts.filter((c) => c.territoryCode === selectedTerritory); const clusters = clusterContacts(filteredGps); // Auto-zoom when contacts load useEffect(() => { if (filteredGps.length === 0 || !mapRef.current) return; const coords = filteredGps.map((c) => ({ latitude: c.latitude as number, longitude: c.longitude as number, })); setTimeout(() => { mapRef.current?.fitToCoordinates(coords, { edgePadding: { top: 80, right: 40, bottom: 300, left: 40 }, animated: true, }); }, 600); }, [filteredGps.length, selectedTerritory]); const handlePinPress = useCallback((contact: Contact) => { setSelectedContact(contact); }, []); const handleViewFull = useCallback( (id: string) => { setSelectedContact(null); router.push(`/contact/${id}` as any); }, [router] ); const handleNoGpsPress = useCallback( (id: string) => { router.push(`/contact/${id}` as any); }, [router] ); if (loading) { return ( Loading map… ); } return ( {/* ── Territory filter chips ── */} {['All', ...territories].map((t) => { const active = selectedTerritory === t; return ( { setSelectedTerritory(t); setSelectedContact(null); }} > {t} ); })} {/* ── Map ── */} setSelectedContact(null)} > {clusters.map((group) => { const isCluster = group.contacts.length > 1; const rep = group.contacts[0]; const pinColor = STATUS_COLORS[rep.status] ?? DEFAULT_PIN_COLOR; if (isCluster) { return ( handlePinPress(rep)} > {group.contacts.length} ); } return ( handlePinPress(rep)} /> ); })} {/* Pin count badge */} {filteredGps.length} pin{filteredGps.length !== 1 ? 's' : ''} {/* Mini card overlay */} {selectedContact && ( setSelectedContact(null)} onViewFull={handleViewFull} /> )} {/* ── No GPS section ── */} {noGpsContacts.length > 0 && ( {noGpsContacts.length} contact{noGpsContacts.length !== 1 ? 's' : ''} without GPS c.id} renderItem={({ item }) => ( )} style={styles.noGpsList} showsVerticalScrollIndicator={false} /> )} ); } // ── Styles ─────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F8F4EF', }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#F8F4EF', }, loadingText: { marginTop: 12, color: '#2C3E50', fontSize: 15, }, // Chip bar chipBar: { backgroundColor: '#F8F4EF', paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#D0CCC6', }, chipScroll: { paddingHorizontal: 12, gap: 8, }, chip: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 20, borderWidth: 1.5, borderColor: '#1A6B72', backgroundColor: 'transparent', }, chipActive: { backgroundColor: '#1A6B72', }, chipText: { fontSize: 13, fontWeight: '600', color: '#1A6B72', }, chipTextActive: { color: '#fff', }, // Map mapContainer: { flex: 1, }, // Pin count badge pinCountBadge: { position: 'absolute', top: 10, right: 12, backgroundColor: 'rgba(26,107,114,0.88)', borderRadius: 12, paddingHorizontal: 10, paddingVertical: 4, }, pinCountText: { color: '#fff', fontSize: 12, fontWeight: '700', }, // Cluster marker clusterMarker: { minWidth: 36, minHeight: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', borderWidth: 2, borderColor: '#fff', paddingHorizontal: 6, }, clusterCount: { color: '#fff', fontWeight: '800', fontSize: 14, }, // Mini card miniCard: { position: 'absolute', bottom: 16, left: 12, right: 12, backgroundColor: '#fff', borderRadius: 16, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, }, miniCardRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, marginBottom: 12, }, miniCardName: { fontSize: 17, fontWeight: '700', color: '#2C3E50', marginBottom: 2, }, miniCardSub: { fontSize: 13, color: '#6B7280', marginTop: 1, }, statusBadge: { borderRadius: 8, paddingHorizontal: 8, paddingVertical: 4, alignSelf: 'flex-start', }, statusBadgeText: { color: '#fff', fontSize: 11, fontWeight: '700', }, miniCardActions: { flexDirection: 'row', alignItems: 'center', gap: 8, }, viewBtn: { flex: 1, backgroundColor: '#1A6B72', borderRadius: 10, paddingVertical: 10, alignItems: 'center', }, viewBtnText: { color: '#fff', fontWeight: '700', fontSize: 14, }, closeBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#F3F4F6', alignItems: 'center', justifyContent: 'center', }, closeBtnText: { fontSize: 16, color: '#6B7280', fontWeight: '700', }, // No GPS section noGpsSection: { maxHeight: 220, backgroundColor: '#F8F4EF', borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#D0CCC6', }, noGpsHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 8, gap: 8, }, noGpsDivider: { flex: 1, height: StyleSheet.hairlineWidth, backgroundColor: '#B0AAA4', }, noGpsTitle: { fontSize: 12, fontWeight: '600', color: '#6B7280', textTransform: 'uppercase', letterSpacing: 0.5, }, noGpsList: { flex: 1, }, noGpsRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10, gap: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#E5E0DA', }, avatar: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', }, avatarText: { color: '#fff', fontWeight: '800', fontSize: 14, }, noGpsName: { fontSize: 14, fontWeight: '600', color: '#2C3E50', }, noGpsSub: { fontSize: 12, color: '#6B7280', marginTop: 1, }, noGpsChevron: { fontSize: 20, color: '#B0AAA4', }, });