From 98fbc944760a694c600662199a2f6bfbf85a2c3b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:51:40 +0000 Subject: [PATCH] feat: Add street address to locations Adds a street_address column to the locations table and updates the LocationSearch component to capture, store, and display full street addresses. This includes database migration, interface updates, and formatter logic. --- src/components/admin/LocationSearch.tsx | 45 ++++++++++--- src/integrations/supabase/types.ts | 3 + src/lib/locationFormatter.ts | 64 +++++++++++++++++++ src/types/location.ts | 6 +- ...2_5a298052-a29e-4c65-a672-e278ac2e8560.sql | 19 ++++++ 5 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 src/lib/locationFormatter.ts create mode 100644 supabase/migrations/20251106135032_5a298052-a29e-4c65-a672-e278ac2e8560.sql diff --git a/src/components/admin/LocationSearch.tsx b/src/components/admin/LocationSearch.tsx index 160dd5ee..edf8fc04 100644 --- a/src/components/admin/LocationSearch.tsx +++ b/src/components/admin/LocationSearch.tsx @@ -14,17 +14,27 @@ interface LocationResult { lat: string; lon: string; address: { + house_number?: string; + road?: string; city?: string; town?: string; village?: string; + municipality?: string; state?: string; + province?: string; + state_district?: string; + county?: string; + region?: string; + territory?: string; country?: string; + country_code?: string; postcode?: string; }; } interface SelectedLocation { name: string; + street_address?: string; city?: string; state_province?: string; country: string; @@ -61,13 +71,14 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className const loadInitialLocation = async (locationId: string): Promise => { const { data, error } = await supabase .from('locations') - .select('id, name, city, state_province, country, postal_code, latitude, longitude, timezone') + .select('id, name, street_address, city, state_province, country, postal_code, latitude, longitude, timezone') .eq('id', locationId) .maybeSingle(); if (data && !error) { setSelectedLocation({ name: data.name, + street_address: data.street_address || undefined, city: data.city || undefined, state_province: data.state_province || undefined, country: data.country, @@ -150,21 +161,38 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className // Safely access address properties with fallback const address = result.address || {}; - const city = address.city || address.town || address.village; - const state = address.state || ''; - const country = address.country || 'Unknown'; - const locationName = city - ? `${city}, ${state} ${country}`.trim() - : result.display_name; + // Extract street address components + const houseNumber = address.house_number || ''; + const road = address.road || ''; + const streetAddress = [houseNumber, road].filter(Boolean).join(' ').trim() || undefined; + + // Extract city + const city = address.city || address.town || address.village || address.municipality; + + // Extract state/province (try multiple fields for international support) + const state = address.state || + address.province || + address.state_district || + address.county || + address.region || + address.territory; + + const country = address.country || 'Unknown'; + const postalCode = address.postcode; + + // Build location name + const locationParts = [streetAddress, city, state, country].filter(Boolean); + const locationName = locationParts.join(', '); // Build location data object (no database operations) const locationData: SelectedLocation = { name: locationName, + street_address: streetAddress, city: city || undefined, state_province: state || undefined, country: country, - postal_code: address.postcode || undefined, + postal_code: postalCode || undefined, latitude, longitude, timezone: undefined, // Will be set by server during approval if needed @@ -249,6 +277,7 @@ export function LocationSearch({ onLocationSelect, initialLocationId, className

{selectedLocation.name}

+ {selectedLocation.street_address &&

Street: {selectedLocation.street_address}

} {selectedLocation.city &&

City: {selectedLocation.city}

} {selectedLocation.state_province &&

State/Province: {selectedLocation.state_province}

}

Country: {selectedLocation.country}

diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 220c1b1f..2ddad322 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1615,6 +1615,7 @@ export type Database = { name: string postal_code: string | null state_province: string | null + street_address: string | null timezone: string | null } Insert: { @@ -1627,6 +1628,7 @@ export type Database = { name: string postal_code?: string | null state_province?: string | null + street_address?: string | null timezone?: string | null } Update: { @@ -1639,6 +1641,7 @@ export type Database = { name?: string postal_code?: string | null state_province?: string | null + street_address?: string | null timezone?: string | null } Relationships: [] diff --git a/src/lib/locationFormatter.ts b/src/lib/locationFormatter.ts new file mode 100644 index 00000000..983b0d79 --- /dev/null +++ b/src/lib/locationFormatter.ts @@ -0,0 +1,64 @@ +/** + * Location Formatting Utilities + * + * Centralized utilities for formatting location data consistently across the app. + */ + +export interface LocationData { + street_address?: string | null; + city?: string | null; + state_province?: string | null; + country?: string | null; + postal_code?: string | null; +} + +/** + * Format location for display + * @param location - Location data object + * @param includeStreet - Whether to include street address in the output + * @returns Formatted location string or null if no location data + */ +export function formatLocationDisplay( + location: LocationData | null | undefined, + includeStreet: boolean = false +): string | null { + if (!location) return null; + + const parts: string[] = []; + + if (includeStreet && location.street_address) { + parts.push(location.street_address); + } + + if (location.city) { + parts.push(location.city); + } + + if (location.state_province) { + parts.push(location.state_province); + } + + if (location.country) { + parts.push(location.country); + } + + return parts.length > 0 ? parts.join(', ') : null; +} + +/** + * Format full address including street + * @param location - Location data object + * @returns Formatted full address or null if no location data + */ +export function formatFullAddress(location: LocationData | null | undefined): string | null { + return formatLocationDisplay(location, true); +} + +/** + * Format location without street address (city, state, country only) + * @param location - Location data object + * @returns Formatted location without street or null if no location data + */ +export function formatLocationShort(location: LocationData | null | undefined): string | null { + return formatLocationDisplay(location, false); +} diff --git a/src/types/location.ts b/src/types/location.ts index f5113453..83c201e3 100644 --- a/src/types/location.ts +++ b/src/types/location.ts @@ -57,11 +57,13 @@ export interface LocationInfoSettings { * Location data structure */ export interface LocationData { + street_address?: string; country?: string; state_province?: string; city?: string; latitude?: number; longitude?: number; + postal_code?: string; } /** @@ -71,10 +73,12 @@ export function isLocationData(data: unknown): data is LocationData { if (typeof data !== 'object' || data === null) return false; const loc = data as Record; return ( + (loc.street_address === undefined || typeof loc.street_address === 'string') && (loc.country === undefined || typeof loc.country === 'string') && (loc.state_province === undefined || typeof loc.state_province === 'string') && (loc.city === undefined || typeof loc.city === 'string') && (loc.latitude === undefined || typeof loc.latitude === 'number') && - (loc.longitude === undefined || typeof loc.longitude === 'number') + (loc.longitude === undefined || typeof loc.longitude === 'number') && + (loc.postal_code === undefined || typeof loc.postal_code === 'string') ); } diff --git a/supabase/migrations/20251106135032_5a298052-a29e-4c65-a672-e278ac2e8560.sql b/supabase/migrations/20251106135032_5a298052-a29e-4c65-a672-e278ac2e8560.sql new file mode 100644 index 00000000..52a16e0a --- /dev/null +++ b/supabase/migrations/20251106135032_5a298052-a29e-4c65-a672-e278ac2e8560.sql @@ -0,0 +1,19 @@ +-- Add street_address column to locations table +ALTER TABLE locations +ADD COLUMN street_address TEXT; + +-- Add comment explaining the column +COMMENT ON COLUMN locations.street_address IS 'Street address including house number and road name (e.g., "375 North Lagoon Drive")'; + +-- Add index for potential searches +CREATE INDEX idx_locations_street_address ON locations(street_address); + +-- Update existing records: extract from name if it looks like an address +-- (This is best-effort cleanup for existing data) +UPDATE locations +SET street_address = CASE + WHEN name ~ '^\d+\s+.*' THEN + regexp_replace(name, ',.*$', '') -- Extract everything before first comma + ELSE NULL +END +WHERE street_address IS NULL; \ No newline at end of file