import { type PokedexEntry, type PokedexEntryDB } from '$lib/models/PokedexEntry'; import { type CatchRecord, type CatchRecordDB } from '$lib/models/CatchRecord'; import { type CombinedData } from '$lib/models/CombinedData'; import type { SupabaseClient } from '@supabase/supabase-js'; class CombinedDataRepository { private static readonly MAX_ROWS_PER_REQUEST = 1000; constructor( private supabase: SupabaseClient, private userId: string | null, private pokedexId: string | null ) {} // Transform Supabase data to match frontend expectations (minimal transformation) private transformPokedexEntry(entry: PokedexEntryDB): PokedexEntry { return { _id: entry.id.toString(), pokedexNumber: entry.pokedexNumber, pokemon: entry.pokemon, form: entry.form || '', canGigantamax: entry.canGigantamax, regionToCatchIn: entry.regionToCatchIn || '', gamesToCatchIn: entry.gamesToCatchIn || [], regionToEvolveIn: entry.regionToEvolveIn || '', evolutionInformation: entry.evolutionInformation || '', catchInformation: entry.catchInformation || [] // Note: Regional dex numbers are stored in separate regional_dex_numbers table }; } private transformCatchRecord(record: CatchRecordDB): CatchRecord { return { _id: record.id, userId: record.userId, pokemonId: record.pokemonId.toString(), pokedexId: record.pokedexId, haveToEvolve: record.haveToEvolve, caught: record.caught, inHome: record.inHome, hasGigantamaxed: record.hasGigantamaxed, personalNotes: record.personalNotes }; } private buildEntriesQuery(enableForms: boolean, region: string, game: string) { let query = this.supabase.from('pokedex_entries').select('*'); if (!enableForms) { // Filter to only base forms (form is NULL) OR Unown "A" (since Unown has no base form) query = query.or('form.is.null,and(pokemon.eq.Unown,form.eq.A)'); } if (region) { query = query.eq('regionToCatchIn', region); } if (game) { query = query.contains('gamesToCatchIn', [game]); } // Stable ordering: national dex number, then base form first, then form name. query = query .order('pokedexNumber', { ascending: true }) .order('form', { ascending: true, nullsFirst: true }); return query; } private async fetchEntriesByRange( from: number, to: number, enableForms: boolean, region: string, game: string ): Promise { const entries: PokedexEntryDB[] = []; let start = from; const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST; while (start <= to) { const end = Math.min(to, start + maxRows - 1); const { data, error } = await this.buildEntriesQuery(enableForms, region, game).range( start, end ); if (error) { console.error('Error finding paginated combined data:', error); return []; } if (!data || data.length === 0) { break; } entries.push(...data); if (data.length < end - start + 1) { break; } start = end + 1; } return entries; } private async fetchAllEntries( enableForms: boolean, region: string, game: string ): Promise { const entries: PokedexEntryDB[] = []; let start = 0; const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST; while (true) { const end = start + maxRows - 1; const { data, error } = await this.buildEntriesQuery(enableForms, region, game).range( start, end ); if (error) { console.error('Error finding combined data:', error); return []; } if (!data || data.length === 0) { break; } entries.push(...data); if (data.length < maxRows) { break; } start = end + 1; } return entries; } private async fetchCatchRecords(entryIds: number[], userId: string): Promise { if (!this.pokedexId) return []; if (entryIds.length === 0) return []; const records: CatchRecordDB[] = []; const chunkSize = 1000; for (let i = 0; i < entryIds.length; i += chunkSize) { const chunk = entryIds.slice(i, i + chunkSize); const { data, error } = await this.supabase .from('catch_records') .select('*') .eq('userId', userId) .eq('pokedexId', this.pokedexId) .in('pokemonId', chunk); if (error) { console.error('Error loading catch records:', error); continue; } if (data) { records.push(...data); } } return records; } async findAllCombinedData( userId: string, enableForms: boolean = true, region: string = '', game: string = '' ): Promise { const entries = await this.fetchAllEntries(enableForms, region, game); if (!entries || entries.length === 0) { return []; } // Get catch records for all entries let catchRecords: CatchRecordDB[] = []; if (userId && this.pokedexId) { const entryIds = entries.map((entry) => entry.id); catchRecords = await this.fetchCatchRecords(entryIds, userId); } // Combine the data exactly like master branch const combinedData = entries.map((entry) => { const userCatchRecord = catchRecords.find((record) => record.pokemonId === entry.id) || null; const transformedEntry = this.transformPokedexEntry(entry); const transformedCatchRecord = userCatchRecord ? this.transformCatchRecord(userCatchRecord) : null; return { pokedexEntry: transformedEntry, catchRecord: transformedCatchRecord }; }); // Box placement is calculated dynamically on the frontend based on the current sort order. return combinedData; } async findCombinedData( userId: string, page: number = 1, limit: number = 20, enableForms: boolean = true, region: string = '', game: string = '' ): Promise { const from = (page - 1) * limit; const to = from + limit - 1; const entries = await this.fetchEntriesByRange(from, to, enableForms, region, game); if (!entries || entries.length === 0) { return []; } // Get catch records for these entries let catchRecords: CatchRecordDB[] = []; if (userId && this.pokedexId) { const entryIds = entries.map((entry) => entry.id); catchRecords = await this.fetchCatchRecords(entryIds, userId); } // Combine the data exactly like master branch const combinedData = entries.map((entry) => { const userCatchRecord = catchRecords.find((record) => record.pokemonId === entry.id) || null; const transformedEntry = this.transformPokedexEntry(entry); const transformedCatchRecord = userCatchRecord ? this.transformCatchRecord(userCatchRecord) : null; return { pokedexEntry: transformedEntry, catchRecord: transformedCatchRecord }; }); // Box placement is calculated dynamically on the frontend based on the current sort order. return combinedData; } async countCombinedData(enableForms: boolean, region: string, game: string): Promise { let query = this.supabase.from('pokedex_entries').select('id', { count: 'exact', head: true }); // Apply same filters as in findCombinedData if (!enableForms) { // Filter to only base forms (form is NULL) OR Unown "A" (since Unown has no base form) query = query.or('form.is.null,and(pokemon.eq.Unown,form.eq.A)'); } if (region) { query = query.eq('regionToCatchIn', region); } if (game) { query = query.contains('gamesToCatchIn', [game]); } const { count, error } = await query; if (error) { console.error('Error counting combined data:', error); return 0; } return count || 0; } } export default CombinedDataRepository;