- Enable all 3 modules via NEXT_PUBLIC_ENABLE_* build args + runtime env - Fix empty-string-to-null for date/optional columns in all module APIs: fischerei (24 fixes), verbandsverwaltung (15 fixes), sitzungsprotokolle (2 fixes) - CACHE_BUST=12 for full rebuild with new feature flags
1459 lines
51 KiB
TypeScript
1459 lines
51 KiB
TypeScript
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<Database>) {
|
|
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<string, unknown> = { 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<string, unknown> = {};
|
|
|
|
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<string, unknown> = { 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<string, unknown> = { 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<string, unknown> = { 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<string, unknown> = {
|
|
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<CreateCatchInput>,
|
|
) {
|
|
const updateData: Record<string, unknown> = {};
|
|
|
|
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<string, unknown> = {};
|
|
|
|
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<string, unknown> = {};
|
|
|
|
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<string, unknown> = {};
|
|
|
|
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<string, unknown> = {};
|
|
|
|
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,
|
|
};
|
|
},
|
|
};
|
|
}
|