import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export function formatDate(dateString: string): string { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleDateString('en-ZA', { day: '2-digit', month: 'short', year: 'numeric', }); } export function formatDateTime(dateString: string): string { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleString('en-ZA', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } export function formatCurrency(amount: number): string { return new Intl.NumberFormat('en-ZA', { style: 'currency', currency: 'ZAR', }).format(amount); } export function getRelativeTime(dateString: string): string { const date = new Date(dateString); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (diffInSeconds < 60) return 'Just now'; if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`; if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`; if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`; return formatDate(dateString); } export function debounce unknown>( func: T, wait: number ): (...args: Parameters) => void { let timeout: ReturnType; return (...args: Parameters) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } export function groupBy(array: T[], key: keyof T): Record { return array.reduce((result, item) => { const groupKey = String(item[key]); if (!result[groupKey]) { result[groupKey] = []; } result[groupKey].push(item); return result; }, {} as Record); } export function calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371; // Earth's radius in km const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * Parse coordinates from a text field. * Supports formats like: * "-33.9249, 18.4241" * "-33.9249 18.4241" * "lat: -33.9249, lng: 18.4241" * Returns null if parsing fails. */ export function parseCoordinates(coordString: string | undefined): { latitude: number; longitude: number } | null { if (!coordString) return null; const matches = coordString.match(/-?\d+\.?\d*/g); if (matches && matches.length >= 2) { const lat = parseFloat(matches[0]); const lng = parseFloat(matches[1]); if (!isNaN(lat) && !isNaN(lng)) { return { latitude: lat, longitude: lng }; } } return null; } export interface ProximityCluster { id: string; items: T[]; center: { latitude: number; longitude: number }; label: string; } export function clusterByProximity( items: T[], radiusKm: number = 2 ): ProximityCluster[] { const withCoords: Array<{ item: T; coords: { latitude: number; longitude: number } }> = []; const withoutCoords: T[] = []; for (const item of items) { const coords = parseCoordinates(item.expand?.supplier?.coordinates); if (coords) { withCoords.push({ item, coords }); } else { withoutCoords.push(item); } } const assigned = new Set(); const clusters: ProximityCluster[] = []; for (const seed of withCoords) { if (assigned.has(seed.item.id)) continue; const clusterItems: Array<{ item: T; coords: { latitude: number; longitude: number } }> = [seed]; assigned.add(seed.item.id); for (const candidate of withCoords) { if (assigned.has(candidate.item.id)) continue; const dist = calculateDistance( seed.coords.latitude, seed.coords.longitude, candidate.coords.latitude, candidate.coords.longitude ); if (dist <= radiusKm) { clusterItems.push(candidate); assigned.add(candidate.item.id); } } const centerLat = clusterItems.reduce((s, p) => s + p.coords.latitude, 0) / clusterItems.length; const centerLng = clusterItems.reduce((s, p) => s + p.coords.longitude, 0) / clusterItems.length; const supplierNames = Array.from( new Set( clusterItems .map(c => c.item.expand?.supplier?.suppliername) .filter((n): n is string => !!n) ) ); clusters.push({ id: `cluster-${clusters.length}`, items: clusterItems.map(c => c.item), center: { latitude: centerLat, longitude: centerLng }, label: supplierNames.length === 1 ? supplierNames[0] : supplierNames.length > 1 ? `${supplierNames[0]} +${supplierNames.length - 1}` : `Stop ${clusters.length + 1}`, }); } if (withoutCoords.length > 0) { clusters.push({ id: 'cluster-no-coords', items: withoutCoords, center: { latitude: 0, longitude: 0 }, label: 'No location', }); } return clusters; }