"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;
}