import type { Database } from '@kit/supabase/database'; import type { SupabaseClient } from '@supabase/supabase-js'; import type { CreateWaterInput, UpdateWaterInput, CreateFishSpeciesInput, UpdateFishSpeciesInput, CreateWaterSpeciesRuleInput, CreateStockingInput, UpdateStockingInput, CreateLeaseInput, UpdateLeaseInput, CreateCatchBookInput, UpdateCatchBookInput, CreateCatchInput, CreatePermitInput, UpdatePermitInput, CreateInspectorAssignmentInput, CreateCompetitionInput, UpdateCompetitionInput, CreateCompetitionParticipantInput, CreateSupplierInput, UpdateSupplierInput, } from '../schema/fischerei.schema'; /** * Factory for the Fischerei (Fishing Association Management) API. */ export function createFischereiApi(client: SupabaseClient) { return { // ===================================================== // Waters // ===================================================== async listWaters( accountId: string, opts?: { search?: string; waterType?: string; archived?: boolean; page?: number; pageSize?: number; }, ) { let query = client .from('waters') .select( 'id, name, short_name, water_type, description, surface_area_ha, length_m, width_m, avg_depth_m, max_depth_m, outflow, location, county, geo_lat, geo_lng, lfv_number, lfv_name, cost_center_id, hejfish_id, is_archived, created_at, updated_at', { count: 'exact' }, ) .eq('account_id', accountId) .order('name'); if (opts?.search) { query = query.or( `name.ilike.%${opts.search}%,short_name.ilike.%${opts.search}%,location.ilike.%${opts.search}%`, ); } if (opts?.waterType) { query = query.eq( 'water_type', opts.waterType as Database['public']['Enums']['water_type'], ); } if (opts?.archived !== undefined) { query = query.eq('is_archived', opts.archived); } else { // By default, hide archived query = query.eq('is_archived', false); } const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async getWater(waterId: string) { const { data, error } = await client .from('waters') .select('*') .eq('id', waterId) .single(); if (error) throw error; return data; }, async createWater(input: CreateWaterInput, userId: string) { const { data, error } = await client .from('waters') .insert({ account_id: input.accountId, name: input.name, short_name: input.shortName || null, water_type: input.waterType, description: input.description || null, surface_area_ha: input.surfaceAreaHa, length_m: input.lengthM, width_m: input.widthM, avg_depth_m: input.avgDepthM, max_depth_m: input.maxDepthM, outflow: input.outflow || null, location: input.location || null, classification_order: input.classificationOrder, county: input.county || null, geo_lat: input.geoLat, geo_lng: input.geoLng, lfv_number: input.lfvNumber, lfv_name: input.lfvName, cost_share_ds: input.costShareDs, cost_share_kalk: input.costShareKalk, electrofishing_permit_requested: input.electrofishingPermitRequested, hejfish_id: input.hejfishId, cost_center_id: input.costCenterId, is_archived: input.isArchived, created_by: userId, updated_by: userId, }) .select() .single(); if (error) throw error; return data; }, async updateWater(input: UpdateWaterInput, userId: string) { const updateData: Record = { updated_by: userId }; if (input.name !== undefined) updateData.name = input.name; if (input.shortName !== undefined) updateData.short_name = input.shortName; if (input.waterType !== undefined) updateData.water_type = input.waterType; if (input.description !== undefined) updateData.description = input.description; if (input.surfaceAreaHa !== undefined) updateData.surface_area_ha = input.surfaceAreaHa; if (input.lengthM !== undefined) updateData.length_m = input.lengthM; if (input.widthM !== undefined) updateData.width_m = input.widthM; if (input.avgDepthM !== undefined) updateData.avg_depth_m = input.avgDepthM; if (input.maxDepthM !== undefined) updateData.max_depth_m = input.maxDepthM; if (input.outflow !== undefined) updateData.outflow = input.outflow; if (input.location !== undefined) updateData.location = input.location; if (input.classificationOrder !== undefined) updateData.classification_order = input.classificationOrder; if (input.county !== undefined) updateData.county = input.county; if (input.geoLat !== undefined) updateData.geo_lat = input.geoLat; if (input.geoLng !== undefined) updateData.geo_lng = input.geoLng; if (input.lfvNumber !== undefined) updateData.lfv_number = input.lfvNumber; if (input.lfvName !== undefined) updateData.lfv_name = input.lfvName; if (input.costShareDs !== undefined) updateData.cost_share_ds = input.costShareDs; if (input.costShareKalk !== undefined) updateData.cost_share_kalk = input.costShareKalk; if (input.electrofishingPermitRequested !== undefined) updateData.electrofishing_permit_requested = input.electrofishingPermitRequested; if (input.hejfishId !== undefined) updateData.hejfish_id = input.hejfishId; if (input.costCenterId !== undefined) updateData.cost_center_id = input.costCenterId; if (input.isArchived !== undefined) updateData.is_archived = input.isArchived; const { data, error } = await client .from('waters') .update(updateData) .eq('id', input.waterId) .select() .single(); if (error) throw error; return data; }, async deleteWater(waterId: string) { // Soft delete — set is_archived = true const { error } = await client .from('waters') .update({ is_archived: true }) .eq('id', waterId); if (error) throw error; }, async getWaterDetail(waterId: string) { // Fetch water + related data in parallel const [waterResult, rulesResult, leasesResult, inspectorsResult, stockingResult] = await Promise.all([ client.from('waters').select('*').eq('id', waterId).single(), client .from('water_species_rules') .select( 'id, water_id, species_id, min_size_cm, protection_period_start, protection_period_end, max_catch_per_day, max_catch_per_year, created_at, fish_species ( id, name, name_latin )', ) .eq('water_id', waterId), client .from('fishing_leases') .select( 'id, lessor_name, start_date, end_date, initial_amount, fixed_annual_increase, percentage_annual_increase, payment_method, is_archived', ) .eq('water_id', waterId) .order('start_date', { ascending: false }), client .from('water_inspectors') .select( 'id, water_id, member_id, assignment_start, assignment_end, created_at, members ( id, first_name, last_name )', ) .eq('water_id', waterId), client .from('fish_stocking') .select( 'id, stocking_date, quantity, weight_kg, age_class, cost_euros, remarks, fish_species ( id, name ), fish_suppliers ( id, name )', ) .eq('water_id', waterId) .order('stocking_date', { ascending: false }) .limit(20), ]); if (waterResult.error) throw waterResult.error; return { water: waterResult.data, speciesRules: rulesResult.data ?? [], leases: leasesResult.data ?? [], inspectors: inspectorsResult.data ?? [], stockingHistory: stockingResult.data ?? [], }; }, // ===================================================== // Fish Species // ===================================================== async listSpecies( accountId: string, opts?: { search?: string; active?: boolean; page?: number; pageSize?: number; }, ) { let query = client .from('fish_species') .select( 'id, name, name_latin, name_local, is_active, max_age_years, max_weight_kg, max_length_cm, protected_min_size_cm, protection_period_start, protection_period_end, spawning_season_start, spawning_season_end, has_special_spawning_season, k_factor_avg, k_factor_min, k_factor_max, price_per_unit, max_catch_per_day, max_catch_per_year, individual_recording, sort_order, created_at, updated_at', { count: 'exact' }, ) .eq('account_id', accountId) .order('sort_order') .order('name'); if (opts?.search) { query = query.or( `name.ilike.%${opts.search}%,name_latin.ilike.%${opts.search}%,name_local.ilike.%${opts.search}%`, ); } if (opts?.active !== undefined) { query = query.eq('is_active', opts.active); } const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 50; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async getSpecies(speciesId: string) { const { data, error } = await client .from('fish_species') .select('*') .eq('id', speciesId) .single(); if (error) throw error; return data; }, async createSpecies(input: CreateFishSpeciesInput) { const { data, error } = await client .from('fish_species') .insert({ account_id: input.accountId, name: input.name, name_latin: input.nameLatin || null, name_local: input.nameLocal || null, is_active: input.isActive, max_age_years: input.maxAgeYears, max_weight_kg: input.maxWeightKg, max_length_cm: input.maxLengthCm, protected_min_size_cm: input.protectedMinSizeCm, protection_period_start: input.protectionPeriodStart || null, protection_period_end: input.protectionPeriodEnd || null, spawning_season_start: input.spawningSeasonStart || null, spawning_season_end: input.spawningSeasonEnd || null, has_special_spawning_season: input.hasSpecialSpawningSeason, k_factor_avg: input.kFactorAvg, k_factor_min: input.kFactorMin, k_factor_max: input.kFactorMax, price_per_unit: input.pricePerUnit, max_catch_per_day: input.maxCatchPerDay, max_catch_per_year: input.maxCatchPerYear, individual_recording: input.individualRecording, }) .select() .single(); if (error) throw error; return data; }, async updateSpecies(input: UpdateFishSpeciesInput) { const updateData: Record = {}; if (input.name !== undefined) updateData.name = input.name; if (input.nameLatin !== undefined) updateData.name_latin = input.nameLatin; if (input.nameLocal !== undefined) updateData.name_local = input.nameLocal; if (input.isActive !== undefined) updateData.is_active = input.isActive; if (input.maxAgeYears !== undefined) updateData.max_age_years = input.maxAgeYears; if (input.maxWeightKg !== undefined) updateData.max_weight_kg = input.maxWeightKg; if (input.maxLengthCm !== undefined) updateData.max_length_cm = input.maxLengthCm; if (input.protectedMinSizeCm !== undefined) updateData.protected_min_size_cm = input.protectedMinSizeCm; if (input.protectionPeriodStart !== undefined) updateData.protection_period_start = input.protectionPeriodStart; if (input.protectionPeriodEnd !== undefined) updateData.protection_period_end = input.protectionPeriodEnd; if (input.spawningSeasonStart !== undefined) updateData.spawning_season_start = input.spawningSeasonStart; if (input.spawningSeasonEnd !== undefined) updateData.spawning_season_end = input.spawningSeasonEnd; if (input.hasSpecialSpawningSeason !== undefined) updateData.has_special_spawning_season = input.hasSpecialSpawningSeason; if (input.kFactorAvg !== undefined) updateData.k_factor_avg = input.kFactorAvg; if (input.kFactorMin !== undefined) updateData.k_factor_min = input.kFactorMin; if (input.kFactorMax !== undefined) updateData.k_factor_max = input.kFactorMax; if (input.pricePerUnit !== undefined) updateData.price_per_unit = input.pricePerUnit; if (input.maxCatchPerDay !== undefined) updateData.max_catch_per_day = input.maxCatchPerDay; if (input.maxCatchPerYear !== undefined) updateData.max_catch_per_year = input.maxCatchPerYear; if (input.individualRecording !== undefined) updateData.individual_recording = input.individualRecording; const { data, error } = await client .from('fish_species') .update(updateData) .eq('id', input.speciesId) .select() .single(); if (error) throw error; return data; }, async deleteSpecies(speciesId: string) { // Hard delete — will fail if catches reference it (RESTRICT FK) const { error } = await client .from('fish_species') .delete() .eq('id', speciesId); if (error) throw error; }, // ===================================================== // Water-Species Rules // ===================================================== async listWaterSpeciesRules(waterId: string) { const { data, error } = await client .from('water_species_rules') .select( 'id, water_id, species_id, min_size_cm, protection_period_start, protection_period_end, max_catch_per_day, max_catch_per_year, created_at, fish_species ( id, name, name_latin )', ) .eq('water_id', waterId); if (error) throw error; return data ?? []; }, async upsertWaterSpeciesRule(input: CreateWaterSpeciesRuleInput) { const { data, error } = await client .from('water_species_rules') .upsert( { water_id: input.waterId, species_id: input.speciesId, min_size_cm: input.minSizeCm, protection_period_start: input.protectionPeriodStart || null, protection_period_end: input.protectionPeriodEnd || null, max_catch_per_day: input.maxCatchPerDay, max_catch_per_year: input.maxCatchPerYear, }, { onConflict: 'water_id,species_id' }, ) .select() .single(); if (error) throw error; return data; }, async deleteWaterSpeciesRule(ruleId: string) { const { error } = await client .from('water_species_rules') .delete() .eq('id', ruleId); if (error) throw error; }, // ===================================================== // Stocking // ===================================================== async listStocking( accountId: string, opts?: { waterId?: string; speciesId?: string; year?: number; page?: number; pageSize?: number; }, ) { let query = client .from('fish_stocking') .select( 'id, water_id, species_id, stocking_date, quantity, weight_kg, age_class, cost_euros, supplier_id, remarks, created_at, waters ( id, name ), fish_species ( id, name ), fish_suppliers ( id, name )', { count: 'exact' }, ) .eq('account_id', accountId) .order('stocking_date', { ascending: false }); if (opts?.waterId) { query = query.eq('water_id', opts.waterId); } if (opts?.speciesId) { query = query.eq('species_id', opts.speciesId); } if (opts?.year) { query = query .gte('stocking_date', `${opts.year}-01-01`) .lte('stocking_date', `${opts.year}-12-31`); } const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async createStocking(input: CreateStockingInput, userId: string) { const { data, error } = await client .from('fish_stocking') .insert({ account_id: input.accountId, water_id: input.waterId, species_id: input.speciesId, stocking_date: input.stockingDate || null, quantity: input.quantity, weight_kg: input.weightKg, age_class: input.ageClass, cost_euros: input.costEuros, supplier_id: input.supplierId, remarks: input.remarks || null, created_by: userId, updated_by: userId, }) .select() .single(); if (error) throw error; return data; }, async updateStocking(input: UpdateStockingInput, userId: string) { const updateData: Record = { updated_by: userId }; if (input.waterId !== undefined) updateData.water_id = input.waterId; if (input.speciesId !== undefined) updateData.species_id = input.speciesId; if (input.stockingDate !== undefined) updateData.stocking_date = input.stockingDate; if (input.quantity !== undefined) updateData.quantity = input.quantity; if (input.weightKg !== undefined) updateData.weight_kg = input.weightKg; if (input.ageClass !== undefined) updateData.age_class = input.ageClass; if (input.costEuros !== undefined) updateData.cost_euros = input.costEuros; if (input.supplierId !== undefined) updateData.supplier_id = input.supplierId; if (input.remarks !== undefined) updateData.remarks = input.remarks; const { data, error } = await client .from('fish_stocking') .update(updateData) .eq('id', input.stockingId) .select() .single(); if (error) throw error; return data; }, async deleteStocking(stockingId: string) { const { error } = await client .from('fish_stocking') .delete() .eq('id', stockingId); if (error) throw error; }, // ===================================================== // Leases // ===================================================== async listLeases( accountId: string, opts?: { waterId?: string; active?: boolean; page?: number; pageSize?: number; }, ) { let query = client .from('fishing_leases') .select( 'id, water_id, lessor_name, lessor_address, lessor_phone, lessor_email, start_date, end_date, duration_years, initial_amount, fixed_annual_increase, percentage_annual_increase, payment_method, account_holder, iban, bic, location_details, special_agreements, is_archived, created_at, updated_at, waters ( id, name )', { count: 'exact' }, ) .eq('account_id', accountId) .order('start_date', { ascending: false }); if (opts?.waterId) { query = query.eq('water_id', opts.waterId); } if (opts?.active) { const today = new Date().toISOString().split('T')[0]!; query = query .eq('is_archived', false) .or(`end_date.is.null,end_date.gte.${today}`); } const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async getLease(leaseId: string) { const { data, error } = await client .from('fishing_leases') .select('*, waters ( id, name )') .eq('id', leaseId) .single(); if (error) throw error; return data; }, async createLease(input: CreateLeaseInput, userId: string) { const { data, error } = await client .from('fishing_leases') .insert({ account_id: input.accountId, water_id: input.waterId, lessor_name: input.lessorName, lessor_address: input.lessorAddress || null, lessor_phone: input.lessorPhone || null, lessor_email: input.lessorEmail || null, start_date: input.startDate || null, end_date: input.endDate || null, duration_years: input.durationYears, initial_amount: input.initialAmount, fixed_annual_increase: input.fixedAnnualIncrease, percentage_annual_increase: input.percentageAnnualIncrease, payment_method: input.paymentMethod, account_holder: input.accountHolder, iban: input.iban, bic: input.bic, location_details: input.locationDetails, special_agreements: input.specialAgreements, is_archived: input.isArchived, created_by: userId, updated_by: userId, }) .select() .single(); if (error) throw error; return data; }, async updateLease(input: UpdateLeaseInput, userId: string) { const updateData: Record = { updated_by: userId }; if (input.waterId !== undefined) updateData.water_id = input.waterId; if (input.lessorName !== undefined) updateData.lessor_name = input.lessorName; if (input.lessorAddress !== undefined) updateData.lessor_address = input.lessorAddress; if (input.lessorPhone !== undefined) updateData.lessor_phone = input.lessorPhone; if (input.lessorEmail !== undefined) updateData.lessor_email = input.lessorEmail; if (input.startDate !== undefined) updateData.start_date = input.startDate; if (input.endDate !== undefined) updateData.end_date = input.endDate; if (input.durationYears !== undefined) updateData.duration_years = input.durationYears; if (input.initialAmount !== undefined) updateData.initial_amount = input.initialAmount; if (input.fixedAnnualIncrease !== undefined) updateData.fixed_annual_increase = input.fixedAnnualIncrease; if (input.percentageAnnualIncrease !== undefined) updateData.percentage_annual_increase = input.percentageAnnualIncrease; if (input.paymentMethod !== undefined) updateData.payment_method = input.paymentMethod; if (input.accountHolder !== undefined) updateData.account_holder = input.accountHolder; if (input.iban !== undefined) updateData.iban = input.iban; if (input.bic !== undefined) updateData.bic = input.bic; if (input.locationDetails !== undefined) updateData.location_details = input.locationDetails; if (input.specialAgreements !== undefined) updateData.special_agreements = input.specialAgreements; if (input.isArchived !== undefined) updateData.is_archived = input.isArchived; const { data, error } = await client .from('fishing_leases') .update(updateData) .eq('id', input.leaseId) .select() .single(); if (error) throw error; return data; }, // ===================================================== // Catch Books // ===================================================== async listCatchBooks( accountId: string, opts?: { year?: number; status?: string; memberId?: string; search?: string; page?: number; pageSize?: number; }, ) { let query = client .from('catch_books') .select( 'id, member_id, year, member_name, member_birth_date, fishing_days_count, total_fish_caught, card_numbers, is_fly_fisher, status, verification, is_checked, is_submitted, submitted_at, is_hejfish, is_empty, not_fished, remarks, created_at, updated_at, members ( id, first_name, last_name )', { count: 'exact' }, ) .eq('account_id', accountId) .order('year', { ascending: false }) .order('member_name'); if (opts?.year) { query = query.eq('year', opts.year); } if (opts?.status) { query = query.eq( 'status', opts.status as Database['public']['Enums']['catch_book_status'], ); } if (opts?.memberId) { query = query.eq('member_id', opts.memberId); } if (opts?.search) { query = query.ilike('member_name', `%${opts.search}%`); } const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async getCatchBook(catchBookId: string) { const { data, error } = await client .from('catch_books') .select('*, members ( id, first_name, last_name )') .eq('id', catchBookId) .single(); if (error) throw error; return data; }, async createCatchBook(input: CreateCatchBookInput, userId: string) { const { data, error } = await client .from('catch_books') .insert({ account_id: input.accountId, member_id: input.memberId, year: input.year, member_name: input.memberName, member_birth_date: input.memberBirthDate || null, fishing_days_count: input.fishingDaysCount, card_numbers: input.cardNumbers, is_fly_fisher: input.isFlyFisher, is_hejfish: input.isHejfish, is_empty: input.isEmpty, not_fished: input.notFished, remarks: input.remarks || null, created_by: userId, updated_by: userId, }) .select() .single(); if (error) throw error; return data; }, async updateCatchBook(input: UpdateCatchBookInput, userId: string) { const updateData: Record = { updated_by: userId }; if (input.memberId !== undefined) updateData.member_id = input.memberId; if (input.year !== undefined) updateData.year = input.year; if (input.memberName !== undefined) updateData.member_name = input.memberName; if (input.memberBirthDate !== undefined) updateData.member_birth_date = input.memberBirthDate; if (input.fishingDaysCount !== undefined) updateData.fishing_days_count = input.fishingDaysCount; if (input.cardNumbers !== undefined) updateData.card_numbers = input.cardNumbers; if (input.isFlyFisher !== undefined) updateData.is_fly_fisher = input.isFlyFisher; if (input.status !== undefined) updateData.status = input.status; if (input.verification !== undefined) updateData.verification = input.verification; if (input.isChecked !== undefined) updateData.is_checked = input.isChecked; if (input.isSubmitted !== undefined) updateData.is_submitted = input.isSubmitted; if (input.isHejfish !== undefined) updateData.is_hejfish = input.isHejfish; if (input.isEmpty !== undefined) updateData.is_empty = input.isEmpty; if (input.notFished !== undefined) updateData.not_fished = input.notFished; if (input.remarks !== undefined) updateData.remarks = input.remarks; const { data, error } = await client .from('catch_books') .update(updateData) .eq('id', input.catchBookId) .select() .single(); if (error) throw error; return data; }, async submitCatchBook(catchBookId: string, userId: string) { const { data, error } = await client .from('catch_books') .update({ status: 'eingereicht' as Database['public']['Enums']['catch_book_status'], is_submitted: true, submitted_at: new Date().toISOString(), updated_by: userId, }) .eq('id', catchBookId) .select() .single(); if (error) throw error; return data; }, async reviewCatchBook( catchBookId: string, userId: string, status: 'akzeptiert' | 'abgelehnt', verification?: string, remarks?: string, ) { const updateData: Record = { status: status as Database['public']['Enums']['catch_book_status'], is_checked: true, updated_by: userId, }; if (verification !== undefined) updateData.verification = verification; if (remarks !== undefined) updateData.remarks = remarks; const { data, error } = await client .from('catch_books') .update(updateData) .eq('id', catchBookId) .select() .single(); if (error) throw error; return data; }, // ===================================================== // Catches // ===================================================== async listCatches( catchBookId: string, opts?: { page?: number; pageSize?: number }, ) { const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 50; const { data, error, count } = await client .from('catches') .select( 'id, catch_book_id, species_id, water_id, member_id, catch_date, quantity, length_cm, weight_g, size_category, gender, is_empty_entry, has_error, hejfish_id, competition_id, competition_participant_id, permit_id, remarks, created_at, fish_species ( id, name ), waters ( id, name )', { count: 'exact' }, ) .eq('catch_book_id', catchBookId) .order('catch_date', { ascending: false }) .range((page - 1) * pageSize, page * pageSize - 1); if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async createCatch(input: CreateCatchInput) { const { data, error } = await client .from('catches') .insert({ catch_book_id: input.catchBookId, species_id: input.speciesId, water_id: input.waterId, member_id: input.memberId, catch_date: input.catchDate || null, quantity: input.quantity, length_cm: input.lengthCm, weight_g: input.weightG, size_category: input.sizeCategory, gender: input.gender, is_empty_entry: input.isEmptyEntry, has_error: input.hasError, hejfish_id: input.hejfishId, competition_id: input.competitionId, competition_participant_id: input.competitionParticipantId, permit_id: input.permitId, remarks: input.remarks || null, }) .select() .single(); if (error) throw error; return data; }, async updateCatch( catchId: string, input: Partial, ) { const updateData: Record = {}; if (input.speciesId !== undefined) updateData.species_id = input.speciesId; if (input.waterId !== undefined) updateData.water_id = input.waterId; if (input.catchDate !== undefined) updateData.catch_date = input.catchDate; if (input.quantity !== undefined) updateData.quantity = input.quantity; if (input.lengthCm !== undefined) updateData.length_cm = input.lengthCm; if (input.weightG !== undefined) updateData.weight_g = input.weightG; if (input.sizeCategory !== undefined) updateData.size_category = input.sizeCategory; if (input.gender !== undefined) updateData.gender = input.gender; if (input.isEmptyEntry !== undefined) updateData.is_empty_entry = input.isEmptyEntry; if (input.hasError !== undefined) updateData.has_error = input.hasError; if (input.remarks !== undefined) updateData.remarks = input.remarks; const { data, error } = await client .from('catches') .update(updateData) .eq('id', catchId) .select() .single(); if (error) throw error; return data; }, async deleteCatch(catchId: string) { const { error } = await client .from('catches') .delete() .eq('id', catchId); if (error) throw error; }, async getCatchStatistics( accountId: string, year?: number, waterId?: string, ) { const { data, error } = await client.rpc('get_catch_statistics', { p_account_id: accountId, p_year: year ?? null, p_water_id: waterId ?? null, }); if (error) throw error; return data ?? []; }, // ===================================================== // Permits // ===================================================== async listPermits( accountId: string, opts?: { archived?: boolean }, ) { let query = client .from('fishing_permits') .select( 'id, name, short_code, primary_water_id, total_quantity, cost_center_id, hejfish_id, is_for_sale, is_archived, created_at, updated_at, waters ( id, name )', ) .eq('account_id', accountId) .order('name'); if (opts?.archived !== undefined) { query = query.eq('is_archived', opts.archived); } else { query = query.eq('is_archived', false); } const { data, error } = await query; if (error) throw error; return data ?? []; }, async createPermit(input: CreatePermitInput) { const { data, error } = await client .from('fishing_permits') .insert({ account_id: input.accountId, name: input.name, short_code: input.shortCode, primary_water_id: input.primaryWaterId, total_quantity: input.totalQuantity, cost_center_id: input.costCenterId, hejfish_id: input.hejfishId, is_for_sale: input.isForSale, is_archived: input.isArchived, }) .select() .single(); if (error) throw error; return data; }, async updatePermit(input: UpdatePermitInput) { const updateData: Record = {}; if (input.name !== undefined) updateData.name = input.name; if (input.shortCode !== undefined) updateData.short_code = input.shortCode; if (input.primaryWaterId !== undefined) updateData.primary_water_id = input.primaryWaterId; if (input.totalQuantity !== undefined) updateData.total_quantity = input.totalQuantity; if (input.costCenterId !== undefined) updateData.cost_center_id = input.costCenterId; if (input.hejfishId !== undefined) updateData.hejfish_id = input.hejfishId; if (input.isForSale !== undefined) updateData.is_for_sale = input.isForSale; if (input.isArchived !== undefined) updateData.is_archived = input.isArchived; const { data, error } = await client .from('fishing_permits') .update(updateData) .eq('id', input.permitId) .select() .single(); if (error) throw error; return data; }, // ===================================================== // Inspectors // ===================================================== async listInspectors(waterId: string) { const { data, error } = await client .from('water_inspectors') .select( 'id, water_id, member_id, assignment_start, assignment_end, created_at, members ( id, first_name, last_name )', ) .eq('water_id', waterId) .order('assignment_start', { ascending: false }); if (error) throw error; return data ?? []; }, async assignInspector(input: CreateInspectorAssignmentInput) { const { data, error } = await client .from('water_inspectors') .insert({ account_id: input.accountId, water_id: input.waterId, member_id: input.memberId, assignment_start: input.assignmentStart, assignment_end: input.assignmentEnd, }) .select() .single(); if (error) throw error; return data; }, async removeInspector(inspectorId: string) { const { error } = await client .from('water_inspectors') .delete() .eq('id', inspectorId); if (error) throw error; }, // ===================================================== // Competitions // ===================================================== async listCompetitions( accountId: string, opts?: { year?: number; page?: number; pageSize?: number }, ) { let query = client .from('competitions') .select( 'id, name, competition_date, event_id, permit_id, water_id, max_participants, score_by_count, score_by_heaviest, score_by_total_weight, score_by_longest, score_by_total_length, separate_member_guest_scoring, result_count_weight, result_count_length, result_count_count, created_at, updated_at, waters ( id, name )', { count: 'exact' }, ) .eq('account_id', accountId) .order('competition_date', { ascending: false }); if (opts?.year) { query = query .gte('competition_date', `${opts.year}-01-01`) .lte('competition_date', `${opts.year}-12-31`); } const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize }; }, async getCompetition(competitionId: string) { const { data, error } = await client .from('competitions') .select('*, waters ( id, name )') .eq('id', competitionId) .single(); if (error) throw error; return data; }, async createCompetition(input: CreateCompetitionInput, userId: string) { const { data, error } = await client .from('competitions') .insert({ account_id: input.accountId, name: input.name, competition_date: input.competitionDate, event_id: input.eventId, permit_id: input.permitId, water_id: input.waterId, max_participants: input.maxParticipants, score_by_count: input.scoreByCount, score_by_heaviest: input.scoreByHeaviest, score_by_total_weight: input.scoreByTotalWeight, score_by_longest: input.scoreByLongest, score_by_total_length: input.scoreByTotalLength, separate_member_guest_scoring: input.separateMemberGuestScoring, result_count_weight: input.resultCountWeight, result_count_length: input.resultCountLength, result_count_count: input.resultCountCount, created_by: userId, }) .select() .single(); if (error) throw error; return data; }, async updateCompetition(input: UpdateCompetitionInput, userId: string) { const updateData: Record = {}; if (input.name !== undefined) updateData.name = input.name; if (input.competitionDate !== undefined) updateData.competition_date = input.competitionDate; if (input.eventId !== undefined) updateData.event_id = input.eventId; if (input.permitId !== undefined) updateData.permit_id = input.permitId; if (input.waterId !== undefined) updateData.water_id = input.waterId; if (input.maxParticipants !== undefined) updateData.max_participants = input.maxParticipants; if (input.scoreByCount !== undefined) updateData.score_by_count = input.scoreByCount; if (input.scoreByHeaviest !== undefined) updateData.score_by_heaviest = input.scoreByHeaviest; if (input.scoreByTotalWeight !== undefined) updateData.score_by_total_weight = input.scoreByTotalWeight; if (input.scoreByLongest !== undefined) updateData.score_by_longest = input.scoreByLongest; if (input.scoreByTotalLength !== undefined) updateData.score_by_total_length = input.scoreByTotalLength; if (input.separateMemberGuestScoring !== undefined) updateData.separate_member_guest_scoring = input.separateMemberGuestScoring; if (input.resultCountWeight !== undefined) updateData.result_count_weight = input.resultCountWeight; if (input.resultCountLength !== undefined) updateData.result_count_length = input.resultCountLength; if (input.resultCountCount !== undefined) updateData.result_count_count = input.resultCountCount; const { data, error } = await client .from('competitions') .update(updateData) .eq('id', input.competitionId) .select() .single(); if (error) throw error; return data; }, async deleteCompetition(competitionId: string) { const { error } = await client .from('competitions') .delete() .eq('id', competitionId); if (error) throw error; }, async listParticipants(competitionId: string) { const { data, error } = await client .from('competition_participants') .select( 'id, competition_id, member_id, category_id, participant_name, birth_date, address, phone, email, participated, total_catch_count, total_weight_g, total_length_cm, heaviest_catch_g, longest_catch_cm, created_at, competition_categories ( id, name )', ) .eq('competition_id', competitionId) .order('participant_name'); if (error) throw error; return data ?? []; }, async addParticipant(input: CreateCompetitionParticipantInput) { const { data, error } = await client .from('competition_participants') .insert({ competition_id: input.competitionId, member_id: input.memberId, category_id: input.categoryId, participant_name: input.participantName, birth_date: input.birthDate, address: input.address, phone: input.phone, email: input.email, }) .select() .single(); if (error) throw error; return data; }, async updateParticipant( participantId: string, updates: { participated?: boolean; totalCatchCount?: number; totalWeightG?: number; totalLengthCm?: number; heaviestCatchG?: number; longestCatchCm?: number; }, ) { const updateData: Record = {}; if (updates.participated !== undefined) updateData.participated = updates.participated; if (updates.totalCatchCount !== undefined) updateData.total_catch_count = updates.totalCatchCount; if (updates.totalWeightG !== undefined) updateData.total_weight_g = updates.totalWeightG; if (updates.totalLengthCm !== undefined) updateData.total_length_cm = updates.totalLengthCm; if (updates.heaviestCatchG !== undefined) updateData.heaviest_catch_g = updates.heaviestCatchG; if (updates.longestCatchCm !== undefined) updateData.longest_catch_cm = updates.longestCatchCm; const { data, error } = await client .from('competition_participants') .update(updateData) .eq('id', participantId) .select() .single(); if (error) throw error; return data; }, async removeParticipant(participantId: string) { const { error } = await client .from('competition_participants') .delete() .eq('id', participantId); if (error) throw error; }, async computeCompetitionResults(competitionId: string) { // Get all participants const { data: participants, error: pError } = await client .from('competition_participants') .select('id, participant_name, member_id, category_id') .eq('competition_id', competitionId); if (pError) throw pError; if (!participants || participants.length === 0) return []; // Get all catches for this competition const { data: catches, error: cError } = await client .from('catches') .select('id, competition_participant_id, quantity, weight_g, length_cm') .eq('competition_id', competitionId) .eq('is_empty_entry', false); if (cError) throw cError; const catchesList = catches ?? []; // Aggregate per participant const results = participants.map((p) => { const pCatches = catchesList.filter( (c) => c.competition_participant_id === p.id, ); const totalCount = pCatches.reduce( (sum, c) => sum + (c.quantity ?? 0), 0, ); const totalWeightG = pCatches.reduce( (sum, c) => sum + (c.weight_g ?? 0), 0, ); const totalLengthCm = pCatches.reduce( (sum, c) => sum + (c.length_cm ?? 0), 0, ); const heaviestG = pCatches.reduce( (max, c) => Math.max(max, c.weight_g ?? 0), 0, ); const longestCm = pCatches.reduce( (max, c) => Math.max(max, c.length_cm ?? 0), 0, ); return { participantId: p.id, participantName: p.participant_name, memberId: p.member_id, categoryId: p.category_id, totalCatchCount: totalCount, totalWeightG, totalLengthCm, heaviestCatchG: heaviestG, longestCatchCm: longestCm, }; }); // Update each participant's results in DB for (const r of results) { await client .from('competition_participants') .update({ participated: true, total_catch_count: r.totalCatchCount, total_weight_g: r.totalWeightG, total_length_cm: r.totalLengthCm, heaviest_catch_g: r.heaviestCatchG, longest_catch_cm: r.longestCatchCm, }) .eq('id', r.participantId); } // Return ranked results (by total weight descending as default) return results.sort((a, b) => b.totalWeightG - a.totalWeightG); }, // ===================================================== // Suppliers // ===================================================== async listSuppliers(accountId: string) { const { data, error } = await client .from('fish_suppliers') .select( 'id, name, contact_person, phone, email, address, notes, is_active, created_at, updated_at', ) .eq('account_id', accountId) .eq('is_active', true) .order('name'); if (error) throw error; return data ?? []; }, async createSupplier(input: CreateSupplierInput) { const { data, error } = await client .from('fish_suppliers') .insert({ account_id: input.accountId, name: input.name, contact_person: input.contactPerson, phone: input.phone, email: input.email, address: input.address, notes: input.notes, is_active: input.isActive, }) .select() .single(); if (error) throw error; return data; }, async updateSupplier(input: UpdateSupplierInput) { const updateData: Record = {}; if (input.name !== undefined) updateData.name = input.name; if (input.contactPerson !== undefined) updateData.contact_person = input.contactPerson; if (input.phone !== undefined) updateData.phone = input.phone; if (input.email !== undefined) updateData.email = input.email; if (input.address !== undefined) updateData.address = input.address; if (input.notes !== undefined) updateData.notes = input.notes; if (input.isActive !== undefined) updateData.is_active = input.isActive; const { data, error } = await client .from('fish_suppliers') .update(updateData) .eq('id', input.supplierId) .select() .single(); if (error) throw error; return data; }, async deleteSupplier(supplierId: string) { const { error } = await client .from('fish_suppliers') .delete() .eq('id', supplierId); if (error) throw error; }, // ===================================================== // Dashboard // ===================================================== async getDashboardStats(accountId: string) { const currentYear = new Date().getFullYear(); const today = new Date().toISOString().split('T')[0]!; const [ watersResult, speciesResult, leasesResult, catchBooksResult, competitionsResult, stockingResult, ] = await Promise.all([ // Waters count (non-archived) client .from('waters') .select('id', { count: 'exact', head: true }) .eq('account_id', accountId) .eq('is_archived', false), // Species count (active) client .from('fish_species') .select('id', { count: 'exact', head: true }) .eq('account_id', accountId) .eq('is_active', true), // Active leases count client .from('fishing_leases') .select('id', { count: 'exact', head: true }) .eq('account_id', accountId) .eq('is_archived', false) .or(`end_date.is.null,end_date.gte.${today}`), // Pending catch books (status = 'offen' or 'eingereicht') client .from('catch_books') .select('id', { count: 'exact', head: true }) .eq('account_id', accountId) .in('status', ['offen', 'eingereicht']), // Upcoming competitions (date >= today) client .from('competitions') .select('id', { count: 'exact', head: true }) .eq('account_id', accountId) .gte('competition_date', today), // Total stocking cost for current year client .from('fish_stocking') .select('cost_euros') .eq('account_id', accountId) .gte('stocking_date', `${currentYear}-01-01`) .lte('stocking_date', `${currentYear}-12-31`), ]); const stockingCostYtd = (stockingResult.data ?? []).reduce( (sum, s) => sum + (Number(s.cost_euros) || 0), 0, ); return { watersCount: watersResult.count ?? 0, speciesCount: speciesResult.count ?? 0, activeLeasesCount: leasesResult.count ?? 0, pendingCatchBooksCount: catchBooksResult.count ?? 0, upcomingCompetitionsCount: competitionsResult.count ?? 0, stockingCostYtd: Math.round(stockingCostYtd * 100) / 100, }; }, }; }