vkashti / app / admin / profiles / ProfilesProvider.tsx
ProfilesProvider.tsx
Raw
"use client";

import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { createClient } from '@/utils/supabase/client';
import { useToast } from '@/components/ui/Toasts/use-toast';
import { Database } from '@/types_db';

// Add debounce utility with cancel property
function debounce<T extends (...args: any[]) => void>(func: T, wait: number): T & { cancel: () => void } {
  let timeout: NodeJS.Timeout;
  const executedFunction = function (...args: Parameters<T>) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  } as T & { cancel: () => void };
  
  executedFunction.cancel = () => {
    clearTimeout(timeout);
  };
  
  return executedFunction;
}

type BaseProfile = Database['public']['Tables']['profiles']['Row'];

export type Profile = BaseProfile & { 
  role?: string;
  created_at?: string;
};

export type ProfileFilters = {
  role: string;
  search: string;
};

export type ProfilesContextType = {
  profiles: Profile[];
  filters: ProfileFilters;
  setFilters: (filters: ProfileFilters) => void;
  debouncedFetchProfiles: () => void;
  updateProfile: (userId: string, updatedFields: Partial<Omit<Profile, 'id' | 'user_id'>>) => void;
  createProfile: () => Promise<string | null>;
  deleteProfile: (userId: string) => void;
};

const ProfilesContext = createContext<ProfilesContextType | null>(null);

export function ProfilesProvider({ children }: { children: React.ReactNode }) {
  const supabase = createClient();
  const { toast } = useToast();
  const [profiles, setProfiles] = useState<Profile[]>([]);
  const [filters, setFilters] = useState<ProfileFilters>({ role: '', search: '' });

  // Create a debounced version of fetchProfiles
  const debouncedFetchProfiles = useCallback(() => {
    const fetchProfilesWithDebounce = debounce(async () => {
      try {
        // Fetch profiles with their roles using a join
        const { data: profilesWithRoles, error } = await supabase
          .from('profiles')
          .select('*, user_roles(role)');

        if (error) {
          throw new Error(error.message);
        }

        // Transform the data to match the expected Profile type
        const transformedProfiles = profilesWithRoles
          .map((profile: any) => ({
            ...profile,
            role: profile.user_roles?.role || ''
          }))
          .sort((a, b) => (a.role || '').localeCompare(b.role || ''));

        // Filter profiles based on search criteria
        const filteredProfiles = transformedProfiles.filter((profile: Profile) => {
          const profileName = `${profile.first_name || ''} ${profile.last_name || ''}`.trim();
          const profileRole = profile.role || '';
          return profileRole.toLowerCase().includes(filters.role.toLowerCase()) &&
                 profileName.toLowerCase().includes(filters.search.toLowerCase());
        });

        setProfiles(filteredProfiles);
      } catch (error) {
        console.error('Error fetching profiles:', error);
        toast({
          title: 'Error fetching profiles',
          description: error instanceof Error ? error.message : 'An unexpected error occurred',
          variant: 'destructive'
        });
      }
    }, 300);

    fetchProfilesWithDebounce();
    
    // Return function to allow cleanup if needed
    return () => {
      fetchProfilesWithDebounce.cancel?.();
    };
  }, [filters, supabase, toast]);

  useEffect(() => {
    debouncedFetchProfiles();
  }, [debouncedFetchProfiles]);

  const updateProfile = async (userId: string, updatedFields: Partial<Omit<Profile, 'id' | 'user_id'>>) => {
    try {
      // Extract role from updated fields
      const { role, ...profileFields } = updatedFields;

      // Update profile
      const { error: profileError } = await supabase
        .from('profiles')
        .update(profileFields)
        .eq('user_id', userId);

      if (profileError) {
        throw new Error(profileError.message);
      }

      // Update role if provided
      if (role !== undefined) {
        // First try to insert
        const { error: insertError } = await supabase
          .from('user_roles')
          .insert({ user_id: userId, role });

        if (insertError?.code === '23505') { // Unique violation error code
          // If insert fails due to unique constraint, try update
          const { error: updateError } = await supabase
            .from('user_roles')
            .update({ role })
            .eq('user_id', userId);

          if (updateError) {
            throw new Error(updateError.message);
          }
        } else if (insertError) {
          throw new Error(insertError.message);
        }
      }

      // Fetch updated profiles
      debouncedFetchProfiles();

      toast({
        title: 'Success',
        description: 'Profile updated successfully',
      });
    } catch (error) {
      console.error('Error updating profile:', error);
      toast({
        title: 'Error updating profile',
        description: error instanceof Error ? error.message : 'An unexpected error occurred',
        variant: 'destructive'
      });
    }
  };

  const createProfile = async (): Promise<string | null> => {
    toast({
      title: 'Not implemented',
      description: 'Creating profiles should be done through Supabase Auth signup or admin.createUser',
      variant: 'destructive'
    });
    return null;
  };

  const deleteProfile = async (userId: string) => {
    try {
      // Delete profile role first (due to foreign key constraint)
      const { error: roleError } = await supabase
        .from('user_roles')
        .delete()
        .eq('user_id', userId);

      if (roleError) {
        throw new Error(roleError.message);
      }

      // Then delete profile
      const { error: profileError } = await supabase
        .from('profiles')
        .delete()
        .eq('user_id', userId);
        
      if (profileError) {
        throw new Error(profileError.message);
      }

      setProfiles((prev) => prev.filter((profile) => profile.user_id !== userId));
      toast({
        title: 'Success',
        description: 'Profile deleted successfully',
      });
    } catch (error) {
      console.error('Error deleting profile:', error);
      toast({
        title: 'Error updating profile',
        description: error instanceof Error ? error.message : 'An unexpected error occurred',
        variant: 'destructive'
      });
    }
  };

  return (
    <ProfilesContext.Provider
      value={{
        profiles,
        filters,
        setFilters,
        debouncedFetchProfiles,
        updateProfile,
        createProfile,
        deleteProfile
      }}
    >
      {children}
    </ProfilesContext.Provider>
  );
}

export function useProfiles() {
  const context = useContext(ProfilesContext);
  if (!context) {
    throw new Error('useProfiles must be used within a ProfilesProvider');
  }
  return context;
}