All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m41s
162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
// src/hooks/useProfileAddress.ts
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import toast from 'react-hot-toast';
|
|
import type { Address, UserProfile } from '../types';
|
|
import { useUserAddressQuery } from './queries/useUserAddressQuery';
|
|
import { useGeocodeMutation } from './mutations/useGeocodeMutation';
|
|
import { logger } from '../services/logger.client';
|
|
import { useDebounce } from './useDebounce';
|
|
import { notifyError } from '../services/notificationService';
|
|
|
|
/**
|
|
* Helper to generate a consistent address string for geocoding.
|
|
*/
|
|
const getAddressString = (address: Partial<Address>): string => {
|
|
return [
|
|
address.address_line_1,
|
|
address.city,
|
|
address.province_state,
|
|
address.postal_code,
|
|
address.country,
|
|
]
|
|
.filter(Boolean)
|
|
.join(', ');
|
|
};
|
|
|
|
/**
|
|
* A custom hook to manage a user's profile address, including fetching,
|
|
* updating, and automatic/manual geocoding.
|
|
*
|
|
* Refactored to use TanStack Query (ADR-0005 Phase 7).
|
|
*
|
|
* @param userProfile The user's profile object.
|
|
* @param isOpen Whether the parent component (e.g., a modal) is open. This is used to reset state.
|
|
* @returns An object with address state and handler functions.
|
|
*/
|
|
export const useProfileAddress = (userProfile: UserProfile | null, isOpen: boolean) => {
|
|
const [address, setAddress] = useState<Partial<Address>>({});
|
|
const [initialAddress, setInitialAddress] = useState<Partial<Address>>({});
|
|
|
|
// TanStack Query for fetching the address
|
|
const {
|
|
data: fetchedAddress,
|
|
isLoading: isFetchingAddress,
|
|
error: addressError,
|
|
} = useUserAddressQuery(userProfile?.address_id, isOpen && !!userProfile?.address_id);
|
|
|
|
// TanStack Query mutation for geocoding
|
|
const geocodeMutation = useGeocodeMutation();
|
|
|
|
// Effect to handle address fetch errors
|
|
useEffect(() => {
|
|
if (addressError) {
|
|
notifyError(addressError.message || 'Failed to fetch address');
|
|
}
|
|
}, [addressError]);
|
|
|
|
// Effect to sync fetched address to local state
|
|
useEffect(() => {
|
|
if (!isOpen || !userProfile) {
|
|
logger.debug(
|
|
'[useProfileAddress] Modal is closed or profile is null. Resetting address state.',
|
|
);
|
|
setAddress({});
|
|
setInitialAddress({});
|
|
return;
|
|
}
|
|
|
|
if (fetchedAddress) {
|
|
logger.debug('[useProfileAddress] Successfully fetched address:', fetchedAddress);
|
|
setAddress(fetchedAddress);
|
|
setInitialAddress(fetchedAddress);
|
|
} else if (!userProfile.address_id) {
|
|
logger.debug('[useProfileAddress] Profile has no address_id. Resetting address form.');
|
|
setAddress({});
|
|
setInitialAddress({});
|
|
} else if (!isFetchingAddress && !fetchedAddress && userProfile.address_id) {
|
|
// Fetch completed but returned null - log a warning
|
|
logger.warn(
|
|
`[useProfileAddress] Fetch returned null for addressId: ${userProfile.address_id}.`,
|
|
);
|
|
}
|
|
}, [isOpen, userProfile, fetchedAddress, isFetchingAddress]);
|
|
|
|
const handleAddressChange = useCallback((field: keyof Address, value: string) => {
|
|
setAddress((prev) => ({ ...prev, [field]: value }));
|
|
}, []);
|
|
|
|
const handleManualGeocode = useCallback(async () => {
|
|
const addressString = getAddressString(address);
|
|
|
|
if (!addressString) {
|
|
toast.error('Please fill in the address fields before geocoding.');
|
|
return;
|
|
}
|
|
|
|
logger.debug(`[useProfileAddress] Manual geocode triggering for: ${addressString}`);
|
|
try {
|
|
const result = await geocodeMutation.mutateAsync(addressString);
|
|
if (result) {
|
|
const { lat, lng } = result;
|
|
setAddress((prev) => ({ ...prev, latitude: lat, longitude: lng }));
|
|
toast.success('Address re-geocoded successfully!');
|
|
}
|
|
} catch (error) {
|
|
// Error is already logged by the mutation, but we could show a toast here if needed
|
|
logger.error('[useProfileAddress] Manual geocode failed:', error);
|
|
}
|
|
}, [address, geocodeMutation]);
|
|
|
|
// --- Automatic Geocoding Logic ---
|
|
const debouncedAddress = useDebounce(address, 1500);
|
|
|
|
useEffect(() => {
|
|
const handleAutoGeocode = async () => {
|
|
logger.debug('[useProfileAddress] Auto-geocode effect triggered by debouncedAddress change');
|
|
|
|
if (JSON.stringify(debouncedAddress) === JSON.stringify(initialAddress)) {
|
|
logger.debug(
|
|
'[useProfileAddress] Skipping auto-geocode: address is unchanged from initial load.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
const addressString = getAddressString(debouncedAddress);
|
|
|
|
// Don't geocode an empty address or if we already have coordinates.
|
|
if (!addressString || (debouncedAddress.latitude && debouncedAddress.longitude)) {
|
|
logger.debug(
|
|
'[useProfileAddress] Skipping auto-geocode: empty string or coordinates already exist',
|
|
{ hasString: !!addressString, hasCoords: !!debouncedAddress.latitude },
|
|
);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`[useProfileAddress] Auto-geocoding: "${addressString}"`);
|
|
try {
|
|
const result = await geocodeMutation.mutateAsync(addressString);
|
|
if (result) {
|
|
logger.debug('[useProfileAddress] Auto-geocode API returned result:', result);
|
|
const { lat, lng } = result;
|
|
setAddress((prev) => ({ ...prev, latitude: lat, longitude: lng }));
|
|
toast.success('Address geocoded successfully!');
|
|
}
|
|
} catch (error) {
|
|
// Error handling - auto-geocode failures are logged but don't block the user
|
|
logger.warn('[useProfileAddress] Auto-geocode failed:', error);
|
|
}
|
|
};
|
|
|
|
handleAutoGeocode();
|
|
}, [debouncedAddress, initialAddress, geocodeMutation]);
|
|
|
|
return {
|
|
address,
|
|
initialAddress,
|
|
isGeocoding: geocodeMutation.isPending,
|
|
isFetchingAddress,
|
|
handleAddressChange,
|
|
handleManualGeocode,
|
|
};
|
|
};
|