vkashti / app / admin / deals / DealsProvider.tsx
DealsProvider.tsx
Raw
'use client';

import { createContext, useState, useContext, useEffect, ReactNode } from 'react';
import { createClient } from '@/utils/supabase/client';
import { Deal, Tag, DealTag } from '@/types/types';

// Use any type for Supabase client to avoid typing issues
const supabase = createClient() as any;

type DealsContextType = {
  deals: Deal[];
  tags: Tag[];
  selectedTags: number[];
  setSelectedTags: (tags: number[]) => void;
  isLoading: boolean;
  createDeal: (dealData: Partial<Deal>) => Promise<void>;
  updateDeal: (id: number, dealData: Partial<Deal>) => Promise<void>;
  deleteDeal: (id: number) => Promise<void>;
  openEditDialog: (deal: Deal) => void;
  selectedDeal: Deal | null;
  setSelectedDeal: (deal: Deal | null) => void;
  isDialogOpen: boolean;
  setIsDialogOpen: (isOpen: boolean) => void;
  loadDeals: () => Promise<void>;
  getDealTags: (dealId: number) => Promise<number[]>;
  updateDealTags: (dealId: number, tagIds: number[]) => Promise<void>;
  createTag: (tagName: string) => Promise<Tag | null>;
  dealTagsMap: Record<number, number[]>;
};

const DealsContext = createContext<DealsContextType | undefined>(undefined);

export const useDeals = () => {
  const context = useContext(DealsContext);
  if (!context) {
    throw new Error('useDeals must be used within a DealsProvider');
  }
  return context;
};

type DealsProviderProps = {
  children: ReactNode;
};

export default function DealsProvider({ children }: DealsProviderProps) {
  const [deals, setDeals] = useState<Deal[]>([]);
  const [tags, setTags] = useState<Tag[]>([]);
  const [selectedTags, setSelectedTags] = useState<number[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [selectedDeal, setSelectedDeal] = useState<Deal | null>(null);
  const [isDialogOpen, setIsDialogOpen] = useState(false);
  const [dealTagsMap, setDealTagsMap] = useState<Record<number, number[]>>({});

  const loadDeals = async () => {
    setIsLoading(true);
    try {
      console.log("Loading all deals and tags...");
      
      // Fetch deals
      const { data: dealsData, error: dealsError } = await supabase
        .from('deals')
        .select('*');

      if (dealsError) {
        console.error('Error loading deals:', dealsError);
        return;
      }

      // Fetch tags
      const { data: tagsData, error: tagsError } = await supabase
        .from('tags')
        .select('*');

      if (tagsError) {
        console.error('Error loading tags:', tagsError);
        return;
      }

      // Fetch all deal_tags to build the relationship map
      const { data: dealTagsData, error: dealTagsError } = await supabase
        .from('deal_tags')
        .select('*');

      if (dealTagsError) {
        console.error('Error loading deal tags:', dealTagsError);
        return;
      }

      // Build a map of deal_id -> tag_ids for efficient filtering
      const tagMap: Record<number, number[]> = {};
      
      // Initialize with empty arrays for all deals to properly cache empty tag lists
      dealsData.forEach((deal: Deal) => {
        tagMap[deal.id] = [];
      });
      
      // Then fill in the actual tag relationships
      dealTagsData.forEach((dt: DealTag) => {
        if (!tagMap[dt.deal_id]) {
          tagMap[dt.deal_id] = [];
        }
        tagMap[dt.deal_id].push(dt.tag_id);
      });

      console.log("Loaded deal-tag map:", tagMap);

      // Process and set the data
      setDeals(dealsData || []);
      setTags(tagsData || []);
      setDealTagsMap(tagMap);
    } catch (error) {
      console.error('Error in loadDeals:', error);
    } finally {
      setIsLoading(false);
    }
  };

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

  // Filter deals by selected tags
  const filteredDeals = selectedTags.length > 0
    ? deals.filter(deal => {
        const dealTags = dealTagsMap[deal.id] || [];
        // Check if any of the selected tags are in this deal's tags
        return selectedTags.some(tagId => dealTags.includes(tagId));
      }).sort((a, b) => a.price - b.price)
    : deals.sort((a, b) => a.price - b.price);

  const createDeal = async (dealData: Partial<Deal>) => {
    try {
      const { error } = await supabase
        .from('deals')
        .insert([dealData]);

      if (error) throw error;
      
      await loadDeals();
    } catch (error) {
      console.error('Error creating deal:', error);
      throw error;
    }
  };

  const updateDeal = async (id: number, dealData: Partial<Deal>) => {
    try {
      const { error } = await supabase
        .from('deals')
        .update(dealData)
        .eq('id', id);

      if (error) throw error;
      
      await loadDeals();
    } catch (error) {
      console.error('Error updating deal:', error);
      throw error;
    }
  };

  const deleteDeal = async (id: number) => {
    try {
      const { error } = await supabase
        .from('deals')
        .delete()
        .eq('id', id);

      if (error) throw error;
      
      await loadDeals();
    } catch (error) {
      console.error('Error deleting deal:', error);
      throw error;
    }
  };

  const getDealTags = async (dealId: number): Promise<number[]> => {
    console.log("Getting tags for deal:", dealId);
    try {
      // Always use cache if available - even for empty arrays
      // This prevents repeated DB calls for deals with no tags
      if (dealTagsMap[dealId] !== undefined) {
        console.log("Using cached tags for deal:", dealId, "Tags:", dealTagsMap[dealId]);
        return dealTagsMap[dealId];
      }

      // Otherwise fetch from the database
      console.log("Fetching tags from database for deal:", dealId);
      const { data, error } = await supabase
        .from('deal_tags')
        .select('tag_id')
        .eq('deal_id', dealId);

      if (error) {
        console.error('Error fetching deal tags:', error);
        return [];
      }

      const tagIds = data.map((dt: DealTag) => dt.tag_id);
      console.log("Retrieved tags from database for deal:", dealId, "Tags:", tagIds);
      
      // Update our map
      setDealTagsMap(prev => ({
        ...prev,
        [dealId]: tagIds
      }));

      return tagIds;
    } catch (error) {
      console.error('Error in getDealTags:', error);
      return [];
    }
  };

  const updateDealTags = async (dealId: number, tagIds: number[]) => {
    console.log("Updating deal tags for deal:", dealId, "with tags:", tagIds);
    try {
      // First, remove all existing tags for this deal
      const { error: deleteError } = await supabase
        .from('deal_tags')
        .delete()
        .eq('deal_id', dealId);

      if (deleteError) {
        console.error("Error deleting existing deal tags:", deleteError);
        throw deleteError;
      }

      console.log("Deleted existing tags for deal:", dealId);

      // If there are no tags to add, we're done
      if (tagIds.length === 0) {
        console.log("No new tags to add for deal:", dealId);
        // Update our local map
        setDealTagsMap(prev => {
          const newMap = { ...prev };
          delete newMap[dealId];
          return newMap;
        });
        return;
      }

      // Create the array of objects to insert
      const tagInserts = tagIds.map(tagId => ({
        deal_id: dealId,
        tag_id: tagId
      }));

      console.log("Inserting new tags for deal:", dealId, "Tags to insert:", tagInserts);

      // Insert the new tags
      const { error: insertError } = await supabase
        .from('deal_tags')
        .insert(tagInserts);

      if (insertError) {
        console.error("Error inserting new deal tags:", insertError);
        throw insertError;
      }

      console.log("Successfully inserted new tags for deal:", dealId);

      // Update our local map - only for this specific deal
      setDealTagsMap(prev => ({
        ...prev,
        [dealId]: tagIds
      }));
      
      // We don't need to reload all deals, just update the local state
      // await loadDeals(); - REMOVED to prevent infinite loop
    } catch (error) {
      console.error('Error updating deal tags:', error);
      throw error;
    }
  };

  const createTag = async (tagName: string): Promise<Tag | null> => {
    try {
      
      // First check if tag with this name already exists
      const { data: existingTag, error: searchError } = await supabase
        .from('tags')
        .select('*')
        .eq('name', tagName)
        .single();

      if (searchError && searchError.code !== 'PGRST116') { // PGRST116 means no rows returned
        console.error('Error checking for existing tag:', searchError);
        return null;
      }

      // If tag already exists, return it
      if (existingTag) {
        return existingTag;
      }

      // Create new tag
      const { data, error } = await supabase
        .from('tags')
        .insert([{ name: tagName, description: '' }])
        .select()
        .single();

      if (error) {
        console.error('Error creating tag:', error);
        return null;
      }

      // Update local tags state
      setTags(prev => {
        const updatedTags = [...prev, data];
        return updatedTags;
      });
      
      return data;
    } catch (error) {
      console.error('Error in createTag:', error);
      return null;
    }
  };

  const openEditDialog = (deal: Deal) => {
    setSelectedDeal(deal);
    setIsDialogOpen(true);
  };

  return (
    <DealsContext.Provider value={{
      deals: filteredDeals,
      tags,
      selectedTags,
      setSelectedTags,
      isLoading,
      createDeal,
      updateDeal,
      deleteDeal,
      openEditDialog,
      selectedDeal,
      setSelectedDeal,
      isDialogOpen, 
      setIsDialogOpen,
      loadDeals,
      getDealTags,
      updateDealTags,
      createTag,
      dealTagsMap
    }}>
      {children}
    </DealsContext.Provider>
  );
}