Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Check } from 'lucide-react';
|
||||
@@ -94,7 +95,7 @@ export function CatchBooksDataTable({
|
||||
<select
|
||||
value={currentYear}
|
||||
onChange={handleYearChange}
|
||||
className="flex h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
|
||||
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
<option value="">Alle Jahre</option>
|
||||
{yearOptions.map((y) => (
|
||||
@@ -107,7 +108,7 @@ export function CatchBooksDataTable({
|
||||
<select
|
||||
value={currentStatus}
|
||||
onChange={handleStatusChange}
|
||||
className="flex h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
|
||||
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
{STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
@@ -126,8 +127,10 @@ export function CatchBooksDataTable({
|
||||
<CardContent>
|
||||
{data.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<h3 className="text-lg font-semibold">Keine Fangbücher vorhanden</h3>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Keine Fangbücher vorhanden
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
||||
Es wurden noch keine Fangbücher angelegt.
|
||||
</p>
|
||||
</div>
|
||||
@@ -135,7 +138,7 @@ export function CatchBooksDataTable({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Mitglied</th>
|
||||
<th className="p-3 text-right font-medium">Jahr</th>
|
||||
<th className="p-3 text-right font-medium">Angeltage</th>
|
||||
@@ -145,7 +148,10 @@ export function CatchBooksDataTable({
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((cb) => {
|
||||
const members = cb.members as Record<string, unknown> | null;
|
||||
const members = cb.members as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const memberName = members
|
||||
? `${String(members.first_name ?? '')} ${String(members.last_name ?? '')}`.trim()
|
||||
: String(cb.member_name ?? '—');
|
||||
@@ -154,7 +160,7 @@ export function CatchBooksDataTable({
|
||||
return (
|
||||
<tr
|
||||
key={String(cb.id)}
|
||||
className="cursor-pointer border-b hover:bg-muted/30"
|
||||
className="hover:bg-muted/30 cursor-pointer border-b"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/home/${account}/fischerei/catch-books/${String(cb.id)}`,
|
||||
@@ -192,14 +198,24 @@ export function CatchBooksDataTable({
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => handlePageChange(page - 1)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={page >= totalPages} onClick={() => handlePageChange(page + 1)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
@@ -69,11 +71,16 @@ export function CompetitionsDataTable({
|
||||
<CardContent className="pt-6">
|
||||
{data.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<h3 className="text-lg font-semibold">Keine Wettbewerbe vorhanden</h3>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Keine Wettbewerbe vorhanden
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
||||
Erstellen Sie Ihren ersten Wettbewerb.
|
||||
</p>
|
||||
<Link href={`/home/${account}/fischerei/competitions/new`} className="mt-4">
|
||||
<Link
|
||||
href={`/home/${account}/fischerei/competitions/new`}
|
||||
className="mt-4"
|
||||
>
|
||||
<Button>Neuer Wettbewerb</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -81,21 +88,26 @@ export function CompetitionsDataTable({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Datum</th>
|
||||
<th className="p-3 text-left font-medium">Gewässer</th>
|
||||
<th className="p-3 text-right font-medium">Max. Teilnehmer</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
Max. Teilnehmer
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((comp) => {
|
||||
const waters = comp.waters as Record<string, unknown> | null;
|
||||
const waters = comp.waters as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={String(comp.id)}
|
||||
className="cursor-pointer border-b hover:bg-muted/30"
|
||||
className="hover:bg-muted/30 cursor-pointer border-b"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/home/${account}/fischerei/competitions/${String(comp.id)}`,
|
||||
@@ -104,9 +116,7 @@ export function CompetitionsDataTable({
|
||||
>
|
||||
<td className="p-3 font-medium">{String(comp.name)}</td>
|
||||
<td className="p-3">
|
||||
{comp.competition_date
|
||||
? new Date(String(comp.competition_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
{formatDate(comp.competition_date as string | null)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{waters ? String(waters.name) : '—'}
|
||||
@@ -126,14 +136,24 @@ export function CompetitionsDataTable({
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => handlePageChange(page - 1)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={page >= totalPages} onClick={() => handlePageChange(page + 1)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { CreateFishSpeciesSchema } from '../schema/fischerei.schema';
|
||||
@@ -43,12 +44,24 @@ export function CreateSpeciesForm({
|
||||
nameLatin: (species?.name_latin as string) ?? '',
|
||||
nameLocal: (species?.name_local as string) ?? '',
|
||||
isActive: species?.is_active != null ? Boolean(species.is_active) : true,
|
||||
protectedMinSizeCm: species?.protected_min_size_cm != null ? Number(species.protected_min_size_cm) : undefined,
|
||||
protectedMinSizeCm:
|
||||
species?.protected_min_size_cm != null
|
||||
? Number(species.protected_min_size_cm)
|
||||
: undefined,
|
||||
protectionPeriodStart: (species?.protection_period_start as string) ?? '',
|
||||
protectionPeriodEnd: (species?.protection_period_end as string) ?? '',
|
||||
maxCatchPerDay: species?.max_catch_per_day != null ? Number(species.max_catch_per_day) : undefined,
|
||||
maxCatchPerYear: species?.max_catch_per_year != null ? Number(species.max_catch_per_year) : undefined,
|
||||
individualRecording: species?.individual_recording != null ? Boolean(species.individual_recording) : false,
|
||||
maxCatchPerDay:
|
||||
species?.max_catch_per_day != null
|
||||
? Number(species.max_catch_per_day)
|
||||
: undefined,
|
||||
maxCatchPerYear:
|
||||
species?.max_catch_per_year != null
|
||||
? Number(species.max_catch_per_year)
|
||||
: undefined,
|
||||
individualRecording:
|
||||
species?.individual_recording != null
|
||||
? Boolean(species.individual_recording)
|
||||
: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -138,7 +151,9 @@ export function CreateSpeciesForm({
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value ? Number(e.target.value) : undefined)
|
||||
field.onChange(
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -194,7 +209,9 @@ export function CreateSpeciesForm({
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value ? Number(e.target.value) : undefined)
|
||||
field.onChange(
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -215,7 +232,9 @@ export function CreateSpeciesForm({
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value ? Number(e.target.value) : undefined)
|
||||
field.onChange(
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { todayISO } from '@kit/shared/dates';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -15,7 +17,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { CreateStockingSchema } from '../schema/fischerei.schema';
|
||||
@@ -42,7 +44,7 @@ export function CreateStockingForm({
|
||||
accountId,
|
||||
waterId: '',
|
||||
speciesId: '',
|
||||
stockingDate: new Date().toISOString().split('T')[0],
|
||||
stockingDate: todayISO(),
|
||||
quantity: 0,
|
||||
weightKg: undefined as number | undefined,
|
||||
ageClass: 'sonstige' as const,
|
||||
@@ -84,7 +86,7 @@ export function CreateStockingForm({
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Gewässer wählen —</option>
|
||||
{waters.map((w) => (
|
||||
@@ -107,7 +109,7 @@ export function CreateStockingForm({
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Fischart wählen —</option>
|
||||
{species.map((s) => (
|
||||
@@ -166,7 +168,9 @@ export function CreateStockingForm({
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value ? Number(e.target.value) : undefined)
|
||||
field.onChange(
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -183,7 +187,7 @@ export function CreateStockingForm({
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="brut">Brut</option>
|
||||
<option value="soemmerlinge">Sömmerlinge</option>
|
||||
@@ -214,7 +218,9 @@ export function CreateStockingForm({
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value ? Number(e.target.value) : undefined)
|
||||
field.onChange(
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -232,7 +238,7 @@ export function CreateStockingForm({
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { CreateWaterSchema } from '../schema/fischerei.schema';
|
||||
@@ -43,18 +44,24 @@ export function CreateWaterForm({
|
||||
shortName: (water?.short_name as string) ?? '',
|
||||
waterType: (water?.water_type as string) ?? 'sonstige',
|
||||
description: (water?.description as string) ?? '',
|
||||
surfaceAreaHa: water?.surface_area_ha != null ? Number(water.surface_area_ha) : undefined,
|
||||
surfaceAreaHa:
|
||||
water?.surface_area_ha != null
|
||||
? Number(water.surface_area_ha)
|
||||
: undefined,
|
||||
lengthM: water?.length_m != null ? Number(water.length_m) : undefined,
|
||||
widthM: water?.width_m != null ? Number(water.width_m) : undefined,
|
||||
avgDepthM: water?.avg_depth_m != null ? Number(water.avg_depth_m) : undefined,
|
||||
maxDepthM: water?.max_depth_m != null ? Number(water.max_depth_m) : undefined,
|
||||
avgDepthM:
|
||||
water?.avg_depth_m != null ? Number(water.avg_depth_m) : undefined,
|
||||
maxDepthM:
|
||||
water?.max_depth_m != null ? Number(water.max_depth_m) : undefined,
|
||||
outflow: (water?.outflow as string) ?? '',
|
||||
location: (water?.location as string) ?? '',
|
||||
county: (water?.county as string) ?? '',
|
||||
geoLat: water?.geo_lat != null ? Number(water.geo_lat) : undefined,
|
||||
geoLng: water?.geo_lng != null ? Number(water.geo_lng) : undefined,
|
||||
lfvNumber: (water?.lfv_number as string) ?? '',
|
||||
costShareDs: water?.cost_share_ds != null ? Number(water.cost_share_ds) : undefined,
|
||||
costShareDs:
|
||||
water?.cost_share_ds != null ? Number(water.cost_share_ds) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -117,7 +124,7 @@ export function CreateWaterForm({
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="fluss">Fluss</option>
|
||||
<option value="bach">Bach</option>
|
||||
@@ -144,7 +151,7 @@ export function CreateWaterForm({
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
Droplets,
|
||||
Fish,
|
||||
FileText,
|
||||
BookOpen,
|
||||
Trophy,
|
||||
Euro,
|
||||
} from 'lucide-react';
|
||||
import { Droplets, Fish, FileText, BookOpen, Trophy, Euro } from 'lucide-react';
|
||||
|
||||
import { formatCurrencyAmount } from '@kit/shared/formatters';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
interface DashboardStats {
|
||||
@@ -27,7 +21,10 @@ interface FischereiDashboardProps {
|
||||
account: string;
|
||||
}
|
||||
|
||||
export function FischereiDashboard({ stats, account }: FischereiDashboardProps) {
|
||||
export function FischereiDashboard({
|
||||
stats,
|
||||
account,
|
||||
}: FischereiDashboardProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Header */}
|
||||
@@ -45,10 +42,12 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Gewässer</p>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Gewässer
|
||||
</p>
|
||||
<p className="text-2xl font-bold">{stats.watersCount}</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-full p-3">
|
||||
<Droplets className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,10 +60,12 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Fischarten</p>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Fischarten
|
||||
</p>
|
||||
<p className="text-2xl font-bold">{stats.speciesCount}</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-full p-3">
|
||||
<Fish className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,10 +78,14 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Aktive Pachten</p>
|
||||
<p className="text-2xl font-bold">{stats.activeLeasesCount}</p>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Aktive Pachten
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.activeLeasesCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-full p-3">
|
||||
<FileText className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,10 +98,14 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Offene Fangbücher</p>
|
||||
<p className="text-2xl font-bold">{stats.pendingCatchBooksCount}</p>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Offene Fangbücher
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.pendingCatchBooksCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-full p-3">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,10 +118,14 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Kommende Wettbewerbe</p>
|
||||
<p className="text-2xl font-bold">{stats.upcomingCompetitionsCount}</p>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Kommende Wettbewerbe
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.upcomingCompetitionsCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-full p-3">
|
||||
<Trophy className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,15 +138,14 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Besatzkosten (lfd. Jahr)</p>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
Besatzkosten (lfd. Jahr)
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{stats.stockingCostYtd.toLocaleString('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
})}
|
||||
{formatCurrencyAmount(stats.stockingCostYtd)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||||
<div className="bg-primary/10 text-primary rounded-full p-3">
|
||||
<Euro className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,7 +161,7 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardTitle className="text-base">Letzte Besatzaktionen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Noch keine Besatzaktionen vorhanden.
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -160,7 +172,7 @@ export function FischereiDashboard({ stats, account }: FischereiDashboardProps)
|
||||
<CardTitle className="text-base">Offene Fangbücher</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Keine Fangbücher zur Prüfung ausstehend.
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
interface FischereiTabNavigationProps {
|
||||
@@ -29,7 +30,10 @@ export function FischereiTabNavigation({
|
||||
|
||||
return (
|
||||
<div className="mb-6 border-b">
|
||||
<nav className="-mb-px flex space-x-1 overflow-x-auto" aria-label="Fischerei Navigation">
|
||||
<nav
|
||||
className="-mb-px flex space-x-1 overflow-x-auto"
|
||||
aria-label="Fischerei Navigation"
|
||||
>
|
||||
{TABS.map((tab) => {
|
||||
const isActive = tab.id === activeTab;
|
||||
|
||||
@@ -38,10 +42,10 @@ export function FischereiTabNavigation({
|
||||
key={tab.id}
|
||||
href={`${basePath}${tab.path}`}
|
||||
className={cn(
|
||||
'whitespace-nowrap border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',
|
||||
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
|
||||
isActive
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground',
|
||||
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -98,11 +99,16 @@ export function SpeciesDataTable({
|
||||
<CardContent>
|
||||
{data.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<h3 className="text-lg font-semibold">Keine Fischarten vorhanden</h3>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Keine Fischarten vorhanden
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
||||
Erstellen Sie Ihre erste Fischart.
|
||||
</p>
|
||||
<Link href={`/home/${account}/fischerei/species/new`} className="mt-4">
|
||||
<Link
|
||||
href={`/home/${account}/fischerei/species/new`}
|
||||
className="mt-4"
|
||||
>
|
||||
<Button>Neue Fischart</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -110,19 +116,28 @@ export function SpeciesDataTable({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Lat. Name</th>
|
||||
<th className="p-3 text-right font-medium">Schonmaß (cm)</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
Schonmaß (cm)
|
||||
</th>
|
||||
<th className="p-3 text-left font-medium">Schonzeit</th>
|
||||
<th className="p-3 text-right font-medium">Max. Fang/Tag</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
Max. Fang/Tag
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((species) => (
|
||||
<tr key={String(species.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3 font-medium">{String(species.name)}</td>
|
||||
<td className="p-3 italic text-muted-foreground">
|
||||
<tr
|
||||
key={String(species.id)}
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-medium">
|
||||
{String(species.name)}
|
||||
</td>
|
||||
<td className="text-muted-foreground p-3 italic">
|
||||
{String(species.name_latin ?? '—')}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
@@ -131,7 +146,8 @@ export function SpeciesDataTable({
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{species.protection_period_start && species.protection_period_end
|
||||
{species.protection_period_start &&
|
||||
species.protection_period_end
|
||||
? `${String(species.protection_period_start)} – ${String(species.protection_period_end)}`
|
||||
: '—'}
|
||||
</td>
|
||||
@@ -149,7 +165,7 @@ export function SpeciesDataTable({
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
import { formatNumber, formatCurrencyAmount } from '@kit/shared/formatters';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
@@ -74,11 +77,16 @@ export function StockingDataTable({
|
||||
<CardContent className="pt-6">
|
||||
{data.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<h3 className="text-lg font-semibold">Keine Besatzeinträge vorhanden</h3>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Keine Besatzeinträge vorhanden
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
||||
Tragen Sie den ersten Besatz ein.
|
||||
</p>
|
||||
<Link href={`/home/${account}/fischerei/stocking/new`} className="mt-4">
|
||||
<Link
|
||||
href={`/home/${account}/fischerei/stocking/new`}
|
||||
className="mt-4"
|
||||
>
|
||||
<Button>Besatz eintragen</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -86,7 +94,7 @@ export function StockingDataTable({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Datum</th>
|
||||
<th className="p-3 text-left font-medium">Gewässer</th>
|
||||
<th className="p-3 text-left font-medium">Fischart</th>
|
||||
@@ -99,33 +107,42 @@ export function StockingDataTable({
|
||||
<tbody>
|
||||
{data.map((row) => {
|
||||
const waters = row.waters as Record<string, unknown> | null;
|
||||
const species = row.fish_species as Record<string, unknown> | null;
|
||||
const species = row.fish_species as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
|
||||
return (
|
||||
<tr key={String(row.id)} className="border-b hover:bg-muted/30">
|
||||
<tr
|
||||
key={String(row.id)}
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3">
|
||||
{row.stocking_date
|
||||
? new Date(String(row.stocking_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
{formatDate(row.stocking_date as string | null)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{waters ? String(waters.name) : '—'}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{species ? String(species.name) : '—'}
|
||||
</td>
|
||||
<td className="p-3">{waters ? String(waters.name) : '—'}</td>
|
||||
<td className="p-3">{species ? String(species.name) : '—'}</td>
|
||||
<td className="p-3 text-right">
|
||||
{Number(row.quantity).toLocaleString('de-DE')}
|
||||
{formatNumber(row.quantity as number)}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
{row.weight_kg != null
|
||||
? `${Number(row.weight_kg).toLocaleString('de-DE')} kg`
|
||||
? `${formatNumber(row.weight_kg as number)} kg`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Badge variant="secondary">
|
||||
{AGE_CLASS_LABELS[String(row.age_class)] ?? String(row.age_class)}
|
||||
{AGE_CLASS_LABELS[String(row.age_class)] ??
|
||||
String(row.age_class)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
{row.cost_euros != null
|
||||
? `${Number(row.cost_euros).toLocaleString('de-DE', { minimumFractionDigits: 2 })} €`
|
||||
? formatCurrencyAmount(row.cost_euros as number)
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -138,14 +155,24 @@ export function StockingDataTable({
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => handlePageChange(page - 1)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page <= 1}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={page >= totalPages} onClick={() => handlePageChange(page + 1)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
>
|
||||
Weiter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { formatNumber } from '@kit/shared/formatters';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
@@ -112,7 +114,7 @@ export function WatersDataTable({
|
||||
<select
|
||||
value={currentType}
|
||||
onChange={handleTypeChange}
|
||||
className="flex h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
|
||||
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
{WATER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
@@ -138,11 +140,16 @@ export function WatersDataTable({
|
||||
<CardContent>
|
||||
{data.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<h3 className="text-lg font-semibold">Keine Gewässer vorhanden</h3>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Keine Gewässer vorhanden
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
||||
Erstellen Sie Ihr erstes Gewässer, um loszulegen.
|
||||
</p>
|
||||
<Link href={`/home/${account}/fischerei/waters/new`} className="mt-4">
|
||||
<Link
|
||||
href={`/home/${account}/fischerei/waters/new`}
|
||||
className="mt-4"
|
||||
>
|
||||
<Button>Neues Gewässer</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -150,7 +157,7 @@ export function WatersDataTable({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Kurzname</th>
|
||||
<th className="p-3 text-left font-medium">Typ</th>
|
||||
@@ -162,7 +169,7 @@ export function WatersDataTable({
|
||||
{data.map((water) => (
|
||||
<tr
|
||||
key={String(water.id)}
|
||||
className="cursor-pointer border-b hover:bg-muted/30"
|
||||
className="hover:bg-muted/30 cursor-pointer border-b"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/home/${account}/fischerei/waters/${String(water.id)}`,
|
||||
@@ -177,7 +184,7 @@ export function WatersDataTable({
|
||||
{String(water.name)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground p-3">
|
||||
{String(water.short_name ?? '—')}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
@@ -188,10 +195,10 @@ export function WatersDataTable({
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
{water.surface_area_ha != null
|
||||
? `${Number(water.surface_area_ha).toLocaleString('de-DE')} ha`
|
||||
? `${formatNumber(water.surface_area_ha as number)} ha`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground p-3">
|
||||
{String(water.location ?? '—')}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -204,7 +211,7 @@ export function WatersDataTable({
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Seite {page} von {totalPages} ({total} Einträge)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -45,10 +45,7 @@ export function isInProtectionPeriod(
|
||||
number,
|
||||
number,
|
||||
];
|
||||
const [endMonth, endDay] = endMMDD.split('.').map(Number) as [
|
||||
number,
|
||||
number,
|
||||
];
|
||||
const [endMonth, endDay] = endMMDD.split('.').map(Number) as [number, number];
|
||||
|
||||
const currentValue = currentMonth * 100 + currentDay;
|
||||
const startValue = startMonth * 100 + startDay;
|
||||
@@ -100,8 +97,7 @@ export function computeLeaseAmountForYear(
|
||||
let amount: number;
|
||||
|
||||
if (percentageIncrease > 0) {
|
||||
amount =
|
||||
initialAmount * Math.pow(1 + percentageIncrease / 100, yearOffset);
|
||||
amount = initialAmount * Math.pow(1 + percentageIncrease / 100, yearOffset);
|
||||
} else {
|
||||
amount = initialAmount + fixedIncrease * yearOffset;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { todayISO } from '@kit/shared/dates';
|
||||
|
||||
// =====================================================
|
||||
// Enum Schemas
|
||||
// =====================================================
|
||||
@@ -51,17 +53,9 @@ export const leasePaymentMethodSchema = z.enum([
|
||||
'ueberweisung',
|
||||
]);
|
||||
|
||||
export const fishGenderSchema = z.enum([
|
||||
'maennlich',
|
||||
'weiblich',
|
||||
'unbekannt',
|
||||
]);
|
||||
export const fishGenderSchema = z.enum(['maennlich', 'weiblich', 'unbekannt']);
|
||||
|
||||
export const fishSizeCategorySchema = z.enum([
|
||||
'gross',
|
||||
'mittel',
|
||||
'klein',
|
||||
]);
|
||||
export const fishSizeCategorySchema = z.enum(['gross', 'mittel', 'klein']);
|
||||
|
||||
// =====================================================
|
||||
// Type exports from enums
|
||||
@@ -153,9 +147,11 @@ export const CreateFishSpeciesSchema = z.object({
|
||||
individualRecording: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const UpdateFishSpeciesSchema = CreateFishSpeciesSchema.partial().extend({
|
||||
speciesId: z.string().uuid(),
|
||||
});
|
||||
export const UpdateFishSpeciesSchema = CreateFishSpeciesSchema.partial().extend(
|
||||
{
|
||||
speciesId: z.string().uuid(),
|
||||
},
|
||||
);
|
||||
|
||||
export type CreateFishSpeciesInput = z.infer<typeof CreateFishSpeciesSchema>;
|
||||
export type UpdateFishSpeciesInput = z.infer<typeof UpdateFishSpeciesSchema>;
|
||||
@@ -322,7 +318,7 @@ export const CreateInspectorAssignmentSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
waterId: z.string().uuid(),
|
||||
memberId: z.string().uuid(),
|
||||
assignmentStart: z.string().default(() => new Date().toISOString().split('T')[0]!),
|
||||
assignmentStart: z.string().default(() => todayISO()),
|
||||
assignmentEnd: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -353,9 +349,11 @@ export const CreateCompetitionSchema = z.object({
|
||||
resultCountCount: z.number().int().default(3),
|
||||
});
|
||||
|
||||
export const UpdateCompetitionSchema = CreateCompetitionSchema.partial().extend({
|
||||
competitionId: z.string().uuid(),
|
||||
});
|
||||
export const UpdateCompetitionSchema = CreateCompetitionSchema.partial().extend(
|
||||
{
|
||||
competitionId: z.string().uuid(),
|
||||
},
|
||||
);
|
||||
|
||||
export type CreateCompetitionInput = z.infer<typeof CreateCompetitionSchema>;
|
||||
export type UpdateCompetitionInput = z.infer<typeof UpdateCompetitionSchema>;
|
||||
@@ -427,4 +425,6 @@ export const CatchStatisticsFilterSchema = z.object({
|
||||
});
|
||||
|
||||
export type FischereiExportInput = z.infer<typeof FischereiExportSchema>;
|
||||
export type CatchStatisticsFilterInput = z.infer<typeof CatchStatisticsFilterSchema>;
|
||||
export type CatchStatisticsFilterInput = z.infer<
|
||||
typeof CatchStatisticsFilterSchema
|
||||
>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
@@ -30,7 +32,6 @@ import {
|
||||
catchBookStatusSchema,
|
||||
catchBookVerificationSchema,
|
||||
} from '../../schema/fischerei.schema';
|
||||
|
||||
import { createFischereiApi } from '../api';
|
||||
|
||||
// =====================================================
|
||||
@@ -197,9 +198,15 @@ export const createStocking = authActionClient
|
||||
const api = createFischereiApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'fischerei.stocking.create' }, 'Creating stocking entry...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.stocking.create' },
|
||||
'Creating stocking entry...',
|
||||
);
|
||||
const result = await api.createStocking(input, userId);
|
||||
logger.info({ name: 'fischerei.stocking.create' }, 'Stocking entry created');
|
||||
logger.info(
|
||||
{ name: 'fischerei.stocking.create' },
|
||||
'Stocking entry created',
|
||||
);
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
@@ -212,9 +219,15 @@ export const updateStocking = authActionClient
|
||||
const api = createFischereiApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'fischerei.stocking.update' }, 'Updating stocking entry...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.stocking.update' },
|
||||
'Updating stocking entry...',
|
||||
);
|
||||
const result = await api.updateStocking(input, userId);
|
||||
logger.info({ name: 'fischerei.stocking.update' }, 'Stocking entry updated');
|
||||
logger.info(
|
||||
{ name: 'fischerei.stocking.update' },
|
||||
'Stocking entry updated',
|
||||
);
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
@@ -231,9 +244,15 @@ export const deleteStocking = authActionClient
|
||||
const logger = await getLogger();
|
||||
const api = createFischereiApi(client);
|
||||
|
||||
logger.info({ name: 'fischerei.stocking.delete' }, 'Deleting stocking entry...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.stocking.delete' },
|
||||
'Deleting stocking entry...',
|
||||
);
|
||||
await api.deleteStocking(input.stockingId);
|
||||
logger.info({ name: 'fischerei.stocking.delete' }, 'Stocking entry deleted');
|
||||
logger.info(
|
||||
{ name: 'fischerei.stocking.delete' },
|
||||
'Stocking entry deleted',
|
||||
);
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
return { success: true };
|
||||
});
|
||||
@@ -284,7 +303,10 @@ export const createCatchBook = authActionClient
|
||||
const api = createFischereiApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'fischerei.catchBook.create' }, 'Creating catch book...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.catchBook.create' },
|
||||
'Creating catch book...',
|
||||
);
|
||||
const result = await api.createCatchBook(input, userId);
|
||||
logger.info({ name: 'fischerei.catchBook.create' }, 'Catch book created');
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
@@ -299,7 +321,10 @@ export const updateCatchBook = authActionClient
|
||||
const api = createFischereiApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'fischerei.catchBook.update' }, 'Updating catch book...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.catchBook.update' },
|
||||
'Updating catch book...',
|
||||
);
|
||||
const result = await api.updateCatchBook(input, userId);
|
||||
logger.info({ name: 'fischerei.catchBook.update' }, 'Catch book updated');
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
@@ -319,7 +344,10 @@ export const submitCatchBook = authActionClient
|
||||
const api = createFischereiApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'fischerei.catchBook.submit' }, 'Submitting catch book...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.catchBook.submit' },
|
||||
'Submitting catch book...',
|
||||
);
|
||||
const result = await api.submitCatchBook(input.catchBookId, userId);
|
||||
logger.info({ name: 'fischerei.catchBook.submit' }, 'Catch book submitted');
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
@@ -456,7 +484,10 @@ export const assignInspector = authActionClient
|
||||
const logger = await getLogger();
|
||||
const api = createFischereiApi(client);
|
||||
|
||||
logger.info({ name: 'fischerei.inspector.assign' }, 'Assigning inspector...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.inspector.assign' },
|
||||
'Assigning inspector...',
|
||||
);
|
||||
const result = await api.assignInspector(input);
|
||||
logger.info({ name: 'fischerei.inspector.assign' }, 'Inspector assigned');
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
@@ -474,7 +505,10 @@ export const removeInspector = authActionClient
|
||||
const logger = await getLogger();
|
||||
const api = createFischereiApi(client);
|
||||
|
||||
logger.info({ name: 'fischerei.inspector.remove' }, 'Removing inspector...');
|
||||
logger.info(
|
||||
{ name: 'fischerei.inspector.remove' },
|
||||
'Removing inspector...',
|
||||
);
|
||||
await api.removeInspector(input.inspectorId);
|
||||
logger.info({ name: 'fischerei.inspector.remove' }, 'Inspector removed');
|
||||
revalidatePath('/home/[account]/fischerei', 'page');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
|
||||
import type {
|
||||
CreateWaterInput,
|
||||
UpdateWaterInput,
|
||||
@@ -132,8 +133,10 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
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.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)
|
||||
@@ -189,37 +192,42 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
|
||||
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),
|
||||
]);
|
||||
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;
|
||||
|
||||
@@ -342,8 +350,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
if (input.spawningSeasonEnd !== undefined)
|
||||
updateData.spawning_season_end = input.spawningSeasonEnd;
|
||||
if (input.hasSpecialSpawningSeason !== undefined)
|
||||
updateData.has_special_spawning_season =
|
||||
input.hasSpecialSpawningSeason;
|
||||
updateData.has_special_spawning_season = input.hasSpecialSpawningSeason;
|
||||
if (input.kFactorAvg !== undefined)
|
||||
updateData.k_factor_avg = input.kFactorAvg;
|
||||
if (input.kFactorMin !== undefined)
|
||||
@@ -741,8 +748,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
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.memberId !== undefined) updateData.member_id = input.memberId;
|
||||
if (input.year !== undefined) updateData.year = input.year;
|
||||
if (input.memberName !== undefined)
|
||||
updateData.member_name = input.memberName;
|
||||
@@ -782,7 +788,8 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
const { data, error } = await client
|
||||
.from('catch_books')
|
||||
.update({
|
||||
status: 'eingereicht' as Database['public']['Enums']['catch_book_status'],
|
||||
status:
|
||||
'eingereicht' as Database['public']['Enums']['catch_book_status'],
|
||||
is_submitted: true,
|
||||
submitted_at: new Date().toISOString(),
|
||||
updated_by: userId,
|
||||
@@ -872,10 +879,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateCatch(
|
||||
catchId: string,
|
||||
input: Partial<CreateCatchInput>,
|
||||
) {
|
||||
async updateCatch(catchId: string, input: Partial<CreateCatchInput>) {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
if (input.speciesId !== undefined)
|
||||
@@ -884,16 +888,14 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
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.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.hasError !== undefined) updateData.has_error = input.hasError;
|
||||
if (input.remarks !== undefined) updateData.remarks = input.remarks;
|
||||
|
||||
const { data, error } = await client
|
||||
@@ -907,10 +909,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
},
|
||||
|
||||
async deleteCatch(catchId: string) {
|
||||
const { error } = await client
|
||||
.from('catches')
|
||||
.delete()
|
||||
.eq('id', catchId);
|
||||
const { error } = await client.from('catches').delete().eq('id', catchId);
|
||||
if (error) throw error;
|
||||
},
|
||||
|
||||
@@ -932,10 +931,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
// Permits
|
||||
// =====================================================
|
||||
|
||||
async listPermits(
|
||||
accountId: string,
|
||||
opts?: { archived?: boolean },
|
||||
) {
|
||||
async listPermits(accountId: string, opts?: { archived?: boolean }) {
|
||||
let query = client
|
||||
.from('fishing_permits')
|
||||
.select(
|
||||
@@ -1121,8 +1117,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
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.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;
|
||||
@@ -1363,8 +1358,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
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;
|
||||
if (input.isActive !== undefined) updateData.is_active = input.isActive;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('fish_suppliers')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" },
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "*.tsx", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user