'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>
);
}