778 lines
23 KiB
TypeScript

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<Database['public']['Tables']['cites_listings']['Row'], 'listing_date'> & {
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<SpeciesDetails | null> {
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<string, Species>();
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<Database['public']['Tables']['cites_listings']['Insert'], 'id'>) {
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<Database['public']['Tables']['timeline_events']['Insert'], 'id'>) {
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<Database['public']['Tables']['iucn_assessments']['Insert'], 'id'>) {
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;
}
}