import { supabase } from './supabase'; import { Database } from '../types/supabase'; import { v4 as uuidv4 } from 'uuid'; export type Species = Database['public']['Tables']['species']['Row'] & { common_names?: CommonName[]; primary_common_name?: string; }; export type CommonName = Database['public']['Tables']['common_names']['Row']; export type Subpopulation = Database['public']['Tables']['subpopulations']['Row']; export type IucnAssessment = Database['public']['Tables']['iucn_assessments']['Row']; // Update the type definition to match the actual database structure export type CitesListing = Omit & { listing_date: string; }; export type CitesTradeRecord = Database['public']['Tables']['cites_trade_records']['Row']; export type TimelineEvent = Database['public']['Tables']['timeline_events']['Row']; export type SpeciesDetails = Species & { common_names: CommonName[]; subpopulations: Subpopulation[]; iucn_assessments: IucnAssessment[]; cites_listings: CitesListing[]; latest_assessment?: IucnAssessment; current_cites_listing?: CitesListing; }; export async function getAllSpecies() { try { console.log("Fetching all species from database..."); // First get all species const { data: allSpecies, error: speciesError } = await supabase .from('species') .select('*') .order('scientific_name'); if (speciesError) { console.error("Error fetching species:", speciesError); throw speciesError; } if (!allSpecies || allSpecies.length === 0) { console.warn("No species found in database!"); return []; } console.log(`Fetched ${allSpecies.length} total species records`); // Manually filter out duplicates by scientific_name const uniqueSpeciesMap = new Map(); allSpecies.forEach(species => { if (!uniqueSpeciesMap.has(species.scientific_name)) { uniqueSpeciesMap.set(species.scientific_name, species); } }); const distinctSpecies = Array.from(uniqueSpeciesMap.values()); console.log(`Filtered to ${distinctSpecies.length} distinct species`); // Then get common names for all species const { data: commonNames, error: commonNamesError } = await supabase .from('common_names') .select('*') .in('species_id', distinctSpecies.map(s => s.id)); if (commonNamesError) { console.error("Error fetching common names:", commonNamesError); throw commonNamesError; } console.log(`Fetched ${commonNames?.length || 0} common names`); // Group common names by species_id const commonNamesBySpecies = new Map(); commonNames?.forEach(cn => { if (!commonNamesBySpecies.has(cn.species_id)) { commonNamesBySpecies.set(cn.species_id, []); } commonNamesBySpecies.get(cn.species_id).push(cn); }); // Transform the data to include primary_common_name const transformedSpecies = distinctSpecies.map(species => ({ ...species, common_names: commonNamesBySpecies.get(species.id) || [], primary_common_name: commonNamesBySpecies.get(species.id)?.[0]?.name || species.common_name || species.scientific_name })); return transformedSpecies; } catch (error) { console.error("Error in getAllSpecies:", error); return []; // Return empty array instead of throwing to avoid breaking the UI } } export async function getSpeciesById(id: string): Promise { try { // Get species basic info const { data: species, error: speciesError } = await supabase .from('species') .select('*') .eq('id', id) .single(); if (speciesError) throw speciesError; if (!species) return null; // Get common names const { data: commonNames, error: commonNamesError } = await supabase .from('common_names') .select('*') .eq('species_id', id); if (commonNamesError) throw commonNamesError; // Get subpopulations const { data: subpopulations, error: subpopulationsError } = await supabase .from('subpopulations') .select('*') .eq('species_id', id); if (subpopulationsError) throw subpopulationsError; // Get IUCN assessments const { data: iucnAssessments, error: iucnError } = await supabase .from('iucn_assessments') .select('*') .eq('species_id', id) .order('year_published', { ascending: false }); if (iucnError) throw iucnError; // Approach 1: Just get all CITES listings without any filtering console.log('Getting all CITES listings'); const { data: allListings, error: allListingsError } = await supabase .from('cites_listings') .select('*'); if (allListingsError) { console.error('Error getting ALL listings:', allListingsError); throw allListingsError; } console.log('All listings in database:', allListings); // Now manually filter to this species const citesListings = allListings.filter(listing => listing.species_id === id ); console.log(`Found ${citesListings.length} listings for species ID ${id}:`, citesListings); // Find latest assessment const latestAssessment = iucnAssessments?.find(a => a.is_latest) || (iucnAssessments && iucnAssessments.length > 0 ? iucnAssessments[0] : undefined); // Set current CITES listing const currentCitesListing = citesListings.find(l => l.is_current) || (citesListings.length > 0 ? citesListings[0] : undefined); // Construct the response return { ...species, common_names: commonNames || [], subpopulations: subpopulations || [], iucn_assessments: iucnAssessments || [], cites_listings: citesListings, latest_assessment: latestAssessment, current_cites_listing: currentCitesListing, }; } catch (error) { console.error('Error in getSpeciesById:', error); throw error; } } export async function getTimelineEvents(speciesId: string) { const { data, error } = await supabase .from('timeline_events') .select('*') .eq('species_id', speciesId) .not('event_type', 'eq', 'cites_trade') .order('event_date', { ascending: false }); if (error) throw error; return data as TimelineEvent[]; } export async function getCitesTradeRecords(speciesId: string) { try { console.log('getCitesTradeRecords called with species ID:', speciesId); // First get the species details to have the scientific name as fallback const { data: speciesData, error: speciesError } = await supabase .from('species') .select('scientific_name, common_name, family, genus, species_name') .eq('id', speciesId) .single(); if (speciesError) { console.error('Error fetching species info:', speciesError); } else { console.log('Found species for trade lookup:', speciesData); } console.log('Fetching all CITES trade records...'); // Initialize array to store all records let allRecords: CitesTradeRecord[] = []; let page = 0; const pageSize = 1000; let hasMore = true; // Fetch records in batches until we have all of them while (hasMore) { const { data: records, error: directError } = await supabase .from('cites_trade_records') .select('*') .eq('species_id', speciesId) .order('year', { ascending: false }) .range(page * pageSize, (page + 1) * pageSize - 1); if (directError) { console.error('Error with direct species_id query:', directError); break; } if (!records || records.length === 0) { hasMore = false; break; } allRecords = [...allRecords, ...records]; console.log(`Fetched batch ${page + 1}, total records so far: ${allRecords.length}`); // If we got less than the page size, we've reached the end if (records.length < pageSize) { hasMore = false; } else { page++; } } if (allRecords.length > 0) { console.log(`Found total of ${allRecords.length} records with direct species_id query`); console.log('First few records:', allRecords.slice(0, 3)); console.log('Year range:', Math.min(...allRecords.map(r => r.year)), 'to', Math.max(...allRecords.map(r => r.year))); return allRecords; } // If no direct matches found, try the fallback approach console.log('No records found with direct query, trying fallback matching...'); // Reset pagination for fallback approach allRecords = []; page = 0; hasMore = true; while (hasMore) { const { data: records, error: fallbackError } = await supabase .from('cites_trade_records') .select('*') .order('year', { ascending: false }) .range(page * pageSize, (page + 1) * pageSize - 1); if (fallbackError) { console.error('Error with fallback query:', fallbackError); break; } if (!records || records.length === 0) { hasMore = false; break; } // Filter records for this batch const filteredRecords = records.filter(record => { // Try scientific name match with the taxon field if (record.taxon && speciesData?.scientific_name && record.taxon.toLowerCase() === speciesData.scientific_name.toLowerCase()) { return true; } // Try family match if (record.family && speciesData?.family && record.family.toLowerCase() === speciesData.family.toLowerCase()) { // For family matches, also check genus if available if (record.genus && speciesData.genus && record.genus.toLowerCase() === speciesData.genus.toLowerCase()) { return true; } } return false; }); allRecords = [...allRecords, ...filteredRecords]; console.log(`Fetched batch ${page + 1}, total filtered records so far: ${allRecords.length}`); if (records.length < pageSize) { hasMore = false; } else { page++; } } if (allRecords.length > 0) { console.log(`Found total of ${allRecords.length} trade records after filtering`); console.log('Year range:', Math.min(...allRecords.map(r => r.year)), 'to', Math.max(...allRecords.map(r => r.year))); } // Sort by year descending allRecords.sort((a, b) => b.year - a.year); return allRecords; } catch (error) { console.error('Error in getCitesTradeRecords:', error); return []; } } // Function to search species by scientific or common name export async function searchSpecies(query: string) { if (!query || query.length < 3) return []; // First search in scientific_name and common_name fields const { data: speciesResults, error: speciesError } = await supabase .from('species') .select('*') .or(`scientific_name.ilike.%${query}%,common_name.ilike.%${query}%`) .limit(20); if (speciesError) throw speciesError; // Also search in common_names table const { data: commonNamesResults, error: commonNamesError } = await supabase .from('common_names') .select('*, species!inner(*)') .ilike('name', `%${query}%`) .limit(20); if (commonNamesError) throw commonNamesError; // Combine the results, removing duplicates const speciesMap = new Map(); if (speciesResults) { for (const species of speciesResults) { speciesMap.set(species.id, species); } } if (commonNamesResults) { for (const result of commonNamesResults) { if (result.species) { speciesMap.set(result.species.id, result.species); } } } return Array.from(speciesMap.values()); } export async function updateSpeciesImage(speciesId: string, imageUrl: string) { const { error } = await supabase .from('species') .update({ default_image_url: imageUrl }) .eq('id', speciesId); if (error) { console.error('Error updating species image:', error); } } export async function getSpeciesImages(scientificName: string) { try { console.log('Fetching images for species:', scientificName); // First try to get from iNaturalist API const response = await fetch( `https://api.inaturalist.org/v1/taxa?q=${encodeURIComponent(scientificName)}&order=desc&order_by=observations_count` ); const data = await response.json(); if (data.results && data.results[0] && data.results[0].default_photo) { console.log('Found image from iNaturalist:', data.results[0].default_photo); return { url: data.results[0].default_photo.medium_url, attribution: data.results[0].default_photo.attribution, license: data.results[0].default_photo.license_code }; } console.log('No image found for species:', scientificName); return null; } catch (error) { console.error('Error fetching species images:', error); return null; } } // CRUD functions for CITES listings export async function createCitesListing(listing: Omit) { try { const newListing = { ...listing, id: uuidv4() }; console.log('Creating new CITES listing:', newListing); const { data, error } = await supabase .from('cites_listings') .insert(newListing) .select() .single(); if (error) { console.error('Error creating CITES listing:', error); throw error; } // If this is set as current, update other listings to not be current if (newListing.is_current) { await updateCitesListingsCurrent(newListing.species_id, newListing.id); } return data; } catch (error) { console.error('Error in createCitesListing:', error); throw error; } } export async function updateCitesListing(id: string, updates: Database['public']['Tables']['cites_listings']['Update']) { try { console.log(`Updating CITES listing ${id}:`, updates); // First get the current listing to check if we need to update current status const { data: currentListing, error: fetchError } = await supabase .from('cites_listings') .select('species_id') .eq('id', id) .single(); if (fetchError) { console.error('Error fetching CITES listing for update:', fetchError); throw fetchError; } // Update the listing const { data, error } = await supabase .from('cites_listings') .update(updates) .eq('id', id) .select() .single(); if (error) { console.error('Error updating CITES listing:', error); throw error; } // If this is set as current, update other listings to not be current if (updates.is_current && currentListing) { await updateCitesListingsCurrent(currentListing.species_id, id); } return data; } catch (error) { console.error('Error in updateCitesListing:', error); throw error; } } export async function deleteCitesListing(id: string) { try { console.log(`Deleting CITES listing ${id}`); // First get the current listing to check if we need to update current status const { data: currentListing, error: fetchError } = await supabase .from('cites_listings') .select('species_id, is_current') .eq('id', id) .single(); if (fetchError) { console.error('Error fetching CITES listing for deletion:', fetchError); throw fetchError; } // Delete the listing const { error } = await supabase .from('cites_listings') .delete() .eq('id', id); if (error) { console.error('Error deleting CITES listing:', error); throw error; } // If this was the current listing, set another one as current if (currentListing && currentListing.is_current) { const { data: remainingListings, error: listingsError } = await supabase .from('cites_listings') .select('id') .eq('species_id', currentListing.species_id) .order('listing_date', { ascending: false }) .limit(1); if (listingsError) { console.error('Error fetching remaining listings:', listingsError); } else if (remainingListings && remainingListings.length > 0) { await updateCitesListing(remainingListings[0].id, { is_current: true }); } } return true; } catch (error) { console.error('Error in deleteCitesListing:', error); throw error; } } // Helper function to update current status of CITES listings async function updateCitesListingsCurrent(speciesId: string, currentId: string) { try { console.log(`Setting listing ${currentId} as current for species ${speciesId}`); const { error } = await supabase .from('cites_listings') .update({ is_current: false }) .eq('species_id', speciesId) .neq('id', currentId); if (error) { console.error('Error updating CITES listings current status:', error); throw error; } return true; } catch (error) { console.error('Error in updateCitesListingsCurrent:', error); throw error; } } // CRUD functions for timeline events export async function createTimelineEvent(event: Omit) { try { const newEvent = { ...event, id: uuidv4() }; console.log('Creating new timeline event:', newEvent); const { data, error } = await supabase .from('timeline_events') .insert(newEvent) .select() .single(); if (error) { console.error('Error creating timeline event:', error); throw error; } return data; } catch (error) { console.error('Error in createTimelineEvent:', error); throw error; } } export async function updateTimelineEvent(id: string, updates: Database['public']['Tables']['timeline_events']['Update']) { try { console.log(`Updating timeline event ${id}:`, updates); const { data, error } = await supabase .from('timeline_events') .update(updates) .eq('id', id) .select() .single(); if (error) { console.error('Error updating timeline event:', error); throw error; } return data; } catch (error) { console.error('Error in updateTimelineEvent:', error); throw error; } } export async function deleteTimelineEvent(id: string) { try { console.log(`Deleting timeline event ${id}`); const { error } = await supabase .from('timeline_events') .delete() .eq('id', id); if (error) { console.error('Error deleting timeline event:', error); throw error; } return true; } catch (error) { console.error('Error in deleteTimelineEvent:', error); throw error; } } // CRUD functions for IUCN assessments export async function createIucnAssessment(assessment: Omit) { try { const newAssessment = { ...assessment, id: uuidv4() }; console.log('Creating new IUCN assessment:', newAssessment); const { data, error } = await supabase .from('iucn_assessments') .insert(newAssessment) .select() .single(); if (error) { console.error('Error creating IUCN assessment:', error); throw error; } // If this is set as latest, update other assessments to not be latest if (newAssessment.is_latest) { await updateIucnAssessmentsLatest(newAssessment.species_id, newAssessment.id); } return data; } catch (error) { console.error('Error in createIucnAssessment:', error); throw error; } } export async function updateIucnAssessment(id: string, updates: Database['public']['Tables']['iucn_assessments']['Update']) { try { console.log(`Updating IUCN assessment ${id}:`, updates); // First get the current assessment to check if we need to update latest status const { data: currentAssessment, error: fetchError } = await supabase .from('iucn_assessments') .select('species_id') .eq('id', id) .single(); if (fetchError) { console.error('Error fetching IUCN assessment for update:', fetchError); throw fetchError; } // Update the assessment const { data, error } = await supabase .from('iucn_assessments') .update(updates) .eq('id', id) .select() .single(); if (error) { console.error('Error updating IUCN assessment:', error); throw error; } // If this is set as latest, update other assessments to not be latest if (updates.is_latest && currentAssessment) { await updateIucnAssessmentsLatest(currentAssessment.species_id, id); } return data; } catch (error) { console.error('Error in updateIucnAssessment:', error); throw error; } } export async function deleteIucnAssessment(id: string) { try { console.log(`Deleting IUCN assessment ${id}`); // First get the current assessment to check if we need to update latest status const { data: currentAssessment, error: fetchError } = await supabase .from('iucn_assessments') .select('species_id, is_latest') .eq('id', id) .single(); if (fetchError) { console.error('Error fetching IUCN assessment for deletion:', fetchError); throw fetchError; } // Delete the assessment const { error } = await supabase .from('iucn_assessments') .delete() .eq('id', id); if (error) { console.error('Error deleting IUCN assessment:', error); throw error; } // If this was the latest assessment, set another one as latest if (currentAssessment && currentAssessment.is_latest) { const { data: remainingAssessments, error: assessmentsError } = await supabase .from('iucn_assessments') .select('id') .eq('species_id', currentAssessment.species_id) .order('year_published', { ascending: false }) .limit(1); if (assessmentsError) { console.error('Error fetching remaining assessments:', assessmentsError); } else if (remainingAssessments && remainingAssessments.length > 0) { await updateIucnAssessment(remainingAssessments[0].id, { is_latest: true }); } } return true; } catch (error) { console.error('Error in deleteIucnAssessment:', error); throw error; } } // Helper function to update latest status of IUCN assessments async function updateIucnAssessmentsLatest(speciesId: string, latestId: string) { try { console.log(`Setting assessment ${latestId} as latest for species ${speciesId}`); const { error } = await supabase .from('iucn_assessments') .update({ is_latest: false }) .eq('species_id', speciesId) .neq('id', latestId); if (error) { console.error('Error updating IUCN assessments latest status:', error); throw error; } return true; } catch (error) { console.error('Error in updateIucnAssessmentsLatest:', error); throw error; } }