All files / src/hooks useProfileAddress.ts

100% Statements 66/66
87.5% Branches 28/32
100% Functions 11/11
100% Lines 63/63

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162                          1x 72x                                         1x 203x 203x             203x     203x     203x 48x 2x         203x 82x 7x     7x 7x 7x     75x 33x 33x 33x 42x 2x 2x 2x 40x   2x           203x 7x     203x 4x   4x 1x 1x     3x 3x 3x 1x 1x 1x 1x       2x         203x   203x 186x 186x   186x 118x     118x     68x     68x 63x       63x     5x 5x 5x 4x 4x 4x 4x 4x       1x       186x     203x                  
// 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);
      Eif (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);
        Eif (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,
  };
};