feat: pre-existing local changes — fischerei, verband, modules, members, packages
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m20s
Workflow / ⚫️ Test (push) Has been skipped

Commits all remaining uncommitted local work:

- apps/web: fischerei, verband, modules, members-cms, documents,
  newsletter, meetings, site-builder, courses, bookings, events,
  finance pages and components
- apps/web: marketing page updates, layout, paths config,
  next.config.mjs, styles/makerkit.css
- apps/web/i18n: documents, fischerei, marketing, verband (de+en)
- packages/features: finance, fischerei, member-management,
  module-builder, newsletter, sitzungsprotokolle, verbandsverwaltung
  server APIs and components
- packages/ui: button.tsx updates
- pnpm-lock.yaml
This commit is contained in:
Zaid Marzguioui
2026-04-02 01:19:54 +02:00
parent a1719671df
commit b26e5aaafa
153 changed files with 2329 additions and 1227 deletions

View File

@@ -42,7 +42,10 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
query = query.ilike('description', `%${opts.search}%`);
}
if (opts?.status) {
query = query.eq('status', opts.status);
query = query.eq(
'status',
opts.status as Database['public']['Enums']['sepa_batch_status'],
);
}
query = query.range((page - 1) * pageSize, page * pageSize - 1);

View File

@@ -154,12 +154,24 @@ export function CatchBooksDataTable({
<table className="w-full text-sm">
<thead>
<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>
<th className="p-3 text-right font-medium">Fänge</th>
<th className="p-3 text-left font-medium">Status</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Mitglied
</th>
<th scope="col" className="p-3 text-right font-medium">
Jahr
</th>
<th scope="col" className="p-3 text-right font-medium">
Angeltage
</th>
<th scope="col" className="p-3 text-right font-medium">
Fänge
</th>
<th scope="col" className="p-3 text-left font-medium">
Status
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -209,6 +221,7 @@ export function CatchBooksDataTable({
<Button
variant="ghost"
size="sm"
aria-label="Fangbuch bearbeiten"
data-test="catchbook-edit-btn"
onClick={(e) => {
e.stopPropagation();
@@ -217,7 +230,7 @@ export function CatchBooksDataTable({
);
}}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-4 w-4" aria-hidden="true" />
</Button>
<DeleteConfirmButton
title="Fangbuch löschen"

View File

@@ -103,13 +103,21 @@ export function CompetitionsDataTable({
<table className="w-full text-sm">
<thead>
<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">
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Datum
</th>
<th scope="col" className="p-3 text-left font-medium">
Gewässer
</th>
<th scope="col" className="p-3 text-right font-medium">
Max. Teilnehmer
</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -146,6 +154,7 @@ export function CompetitionsDataTable({
<Button
variant="ghost"
size="sm"
aria-label="Wettkampf bearbeiten"
data-test="competition-edit-btn"
onClick={(e) => {
e.stopPropagation();
@@ -154,7 +163,7 @@ export function CompetitionsDataTable({
);
}}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-4 w-4" aria-hidden="true" />
</Button>
<DeleteConfirmButton
title="Wettbewerb löschen"

View File

@@ -19,7 +19,7 @@ import {
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import { CreateWaterSchema } from '../schema/fischerei.schema';
import { CreateWaterSchema, type WaterType } from '../schema/fischerei.schema';
import { createWater, updateWater } from '../server/actions/fischerei-actions';
interface CreateWaterFormProps {
@@ -42,7 +42,7 @@ export function CreateWaterForm({
accountId,
name: (water?.name as string) ?? '',
shortName: (water?.short_name as string) ?? '',
waterType: (water?.water_type as string) ?? 'sonstige',
waterType: (water?.water_type as WaterType | undefined) ?? 'sonstige',
description: (water?.description as string) ?? '',
surfaceAreaHa:
water?.surface_area_ha != null

View File

@@ -39,11 +39,12 @@ export function DeleteConfirmButton({
<Button
variant="ghost"
size="sm"
aria-label="Löschen"
data-test="delete-btn"
disabled={isPending}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
>
<Trash2 className="text-destructive h-4 w-4" />
<Trash2 className="text-destructive h-4 w-4" aria-hidden="true" />
</Button>
}
/>

View File

@@ -50,15 +50,27 @@ export function LeasesDataTable({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Verpächter</th>
<th className="p-3 text-left font-medium">Gewässer</th>
<th className="p-3 text-left font-medium">Beginn</th>
<th className="p-3 text-left font-medium">Ende</th>
<th className="p-3 text-right font-medium">
<th scope="col" className="p-3 text-left font-medium">
Verpächter
</th>
<th scope="col" className="p-3 text-left font-medium">
Gewässer
</th>
<th scope="col" className="p-3 text-left font-medium">
Beginn
</th>
<th scope="col" className="p-3 text-left font-medium">
Ende
</th>
<th scope="col" className="p-3 text-right font-medium">
Jahresbetrag (EUR)
</th>
<th className="p-3 text-left font-medium">Zahlungsart</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Zahlungsart
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>

View File

@@ -44,12 +44,24 @@ export function PermitsDataTable({ data, accountId }: PermitsDataTableProps) {
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Bezeichnung</th>
<th className="p-3 text-left font-medium">Kurzcode</th>
<th className="p-3 text-left font-medium">Hauptgewässer</th>
<th className="p-3 text-right font-medium">Gesamtmenge</th>
<th className="p-3 text-center font-medium">Zum Verkauf</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Bezeichnung
</th>
<th scope="col" className="p-3 text-left font-medium">
Kurzcode
</th>
<th scope="col" className="p-3 text-left font-medium">
Hauptgewässer
</th>
<th scope="col" className="p-3 text-right font-medium">
Gesamtmenge
</th>
<th scope="col" className="p-3 text-center font-medium">
Zum Verkauf
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>

View File

@@ -137,16 +137,24 @@ export function SpeciesDataTable({
<table className="w-full text-sm">
<thead>
<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">
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Lat. Name
</th>
<th scope="col" 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">
<th scope="col" className="p-3 text-left font-medium">
Schonzeit
</th>
<th scope="col" className="p-3 text-right font-medium">
Max. Fang/Tag
</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -182,6 +190,7 @@ export function SpeciesDataTable({
<Button
variant="ghost"
size="sm"
aria-label="Fischart bearbeiten"
data-test="species-edit-btn"
onClick={() =>
router.push(
@@ -189,7 +198,7 @@ export function SpeciesDataTable({
)
}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-4 w-4" aria-hidden="true" />
</Button>
<DeleteConfirmButton
title="Fischart löschen"

View File

@@ -108,14 +108,30 @@ export function StockingDataTable({
<table className="w-full text-sm">
<thead>
<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>
<th className="p-3 text-right font-medium">Anzahl</th>
<th className="p-3 text-right font-medium">Gewicht (kg)</th>
<th className="p-3 text-left font-medium">Altersklasse</th>
<th className="p-3 text-right font-medium">Kosten ()</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Datum
</th>
<th scope="col" className="p-3 text-left font-medium">
Gewässer
</th>
<th scope="col" className="p-3 text-left font-medium">
Fischart
</th>
<th scope="col" className="p-3 text-right font-medium">
Anzahl
</th>
<th scope="col" className="p-3 text-right font-medium">
Gewicht (kg)
</th>
<th scope="col" className="p-3 text-left font-medium">
Altersklasse
</th>
<th scope="col" className="p-3 text-right font-medium">
Kosten ()
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -164,6 +180,7 @@ export function StockingDataTable({
<Button
variant="ghost"
size="sm"
aria-label="Besatz bearbeiten"
data-test="stocking-edit-btn"
onClick={() =>
router.push(
@@ -171,7 +188,7 @@ export function StockingDataTable({
)
}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-4 w-4" aria-hidden="true" />
</Button>
<DeleteConfirmButton
title="Besatz löschen"

View File

@@ -178,12 +178,24 @@ export function WatersDataTable({
<table className="w-full text-sm">
<thead>
<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>
<th className="p-3 text-right font-medium">Fläche (ha)</th>
<th className="p-3 text-left font-medium">Ort</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Kurzname
</th>
<th scope="col" className="p-3 text-left font-medium">
Typ
</th>
<th scope="col" className="p-3 text-right font-medium">
Fläche (ha)
</th>
<th scope="col" className="p-3 text-left font-medium">
Ort
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -227,6 +239,7 @@ export function WatersDataTable({
<Button
variant="ghost"
size="sm"
aria-label="Gewässer bearbeiten"
data-test="water-edit-btn"
onClick={(e) => {
e.stopPropagation();
@@ -235,7 +248,7 @@ export function WatersDataTable({
);
}}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-4 w-4" aria-hidden="true" />
</Button>
<DeleteConfirmButton
title="Gewässer löschen"

View File

@@ -490,13 +490,13 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
account_id: input.accountId,
water_id: input.waterId,
species_id: input.speciesId,
stocking_date: input.stockingDate || null,
stocking_date: input.stockingDate,
quantity: input.quantity,
weight_kg: input.weightKg,
age_class: input.ageClass,
cost_euros: input.costEuros,
supplier_id: input.supplierId,
remarks: input.remarks || null,
remarks: input.remarks ?? null,
created_by: userId,
updated_by: userId,
})
@@ -599,11 +599,11 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
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,
lessor_address: input.lessorAddress ?? null,
lessor_phone: input.lessorPhone ?? null,
lessor_email: input.lessorEmail ?? null,
start_date: input.startDate,
end_date: input.endDate ?? null,
duration_years: input.durationYears,
initial_amount: input.initialAmount,
fixed_annual_increase: input.fixedAnnualIncrease,
@@ -885,7 +885,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
species_id: input.speciesId,
water_id: input.waterId,
member_id: input.memberId,
catch_date: input.catchDate || null,
catch_date: input.catchDate,
quantity: input.quantity,
length_cm: input.lengthCm,
weight_g: input.weightG,
@@ -897,7 +897,7 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
competition_id: input.competitionId,
competition_participant_id: input.competitionParticipantId,
permit_id: input.permitId,
remarks: input.remarks || null,
remarks: input.remarks ?? null,
})
.select()
.single();
@@ -946,8 +946,8 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
) {
const { data, error } = await client.rpc('get_catch_statistics', {
p_account_id: accountId,
p_year: year ?? null,
p_water_id: waterId ?? null,
p_year: year,
p_water_id: waterId,
});
if (error) throw error;
return data ?? [];

View File

@@ -103,11 +103,21 @@ export function ApplicationWorkflow({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="px-4 py-3 text-left font-medium">Name</th>
<th className="px-4 py-3 text-left font-medium">E-Mail</th>
<th className="px-4 py-3 text-left font-medium">Datum</th>
<th className="px-4 py-3 text-left font-medium">Status</th>
<th className="px-4 py-3 text-right font-medium">Aktionen</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Name
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
E-Mail
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Datum
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Status
</th>
<th scope="col" className="px-4 py-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>

View File

@@ -205,12 +205,24 @@ export function DuesCategoryManager({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="px-4 py-3 text-left font-medium">Name</th>
<th className="px-4 py-3 text-left font-medium">Beschreibung</th>
<th className="px-4 py-3 text-right font-medium">Betrag</th>
<th className="px-4 py-3 text-left font-medium">Intervall</th>
<th className="px-4 py-3 text-center font-medium">Standard</th>
<th className="px-4 py-3 text-right font-medium">Aktionen</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Name
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Beschreibung
</th>
<th scope="col" className="px-4 py-3 text-right font-medium">
Betrag
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Intervall
</th>
<th scope="col" className="px-4 py-3 text-center font-medium">
Standard
</th>
<th scope="col" className="px-4 py-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>

View File

@@ -238,13 +238,27 @@ export function MandateManager({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="px-4 py-3 text-left font-medium">Referenz</th>
<th className="px-4 py-3 text-left font-medium">IBAN</th>
<th className="px-4 py-3 text-left font-medium">Kontoinhaber</th>
<th className="px-4 py-3 text-left font-medium">Datum</th>
<th className="px-4 py-3 text-left font-medium">Status</th>
<th className="px-4 py-3 text-center font-medium">Primär</th>
<th className="px-4 py-3 text-right font-medium">Aktionen</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Referenz
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
IBAN
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Kontoinhaber
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Datum
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Status
</th>
<th scope="col" className="px-4 py-3 text-center font-medium">
Primär
</th>
<th scope="col" className="px-4 py-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>

View File

@@ -458,10 +458,18 @@ function RolesSection({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-2 text-left font-medium">Bezeichnung</th>
<th className="p-2 text-left font-medium">Von</th>
<th className="p-2 text-left font-medium">Bis</th>
<th className="p-2 text-left font-medium">Aktionen</th>
<th scope="col" className="p-2 text-left font-medium">
Bezeichnung
</th>
<th scope="col" className="p-2 text-left font-medium">
Von
</th>
<th scope="col" className="p-2 text-left font-medium">
Bis
</th>
<th scope="col" className="p-2 text-left font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -639,10 +647,18 @@ function HonorsSection({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-2 text-left font-medium">Bezeichnung</th>
<th className="p-2 text-left font-medium">Datum</th>
<th className="p-2 text-left font-medium">Beschreibung</th>
<th className="p-2 text-left font-medium">Aktionen</th>
<th scope="col" className="p-2 text-left font-medium">
Bezeichnung
</th>
<th scope="col" className="p-2 text-left font-medium">
Datum
</th>
<th scope="col" className="p-2 text-left font-medium">
Beschreibung
</th>
<th scope="col" className="p-2 text-left font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -914,12 +930,24 @@ function MandatesSection({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-2 text-left font-medium">Referenz</th>
<th className="p-2 text-left font-medium">IBAN</th>
<th className="p-2 text-left font-medium">Kontoinhaber</th>
<th className="p-2 text-left font-medium">Datum</th>
<th className="p-2 text-left font-medium">Status</th>
<th className="p-2 text-left font-medium">Aktionen</th>
<th scope="col" className="p-2 text-left font-medium">
Referenz
</th>
<th scope="col" className="p-2 text-left font-medium">
IBAN
</th>
<th scope="col" className="p-2 text-left font-medium">
Kontoinhaber
</th>
<th scope="col" className="p-2 text-left font-medium">
Datum
</th>
<th scope="col" className="p-2 text-left font-medium">
Status
</th>
<th scope="col" className="p-2 text-left font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>

View File

@@ -280,7 +280,9 @@ export function MemberImportWizard({ accountId, account }: Props) {
<table className="w-full text-xs">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-2 text-left">#</th>
<th scope="col" className="p-2 text-left">
#
</th>
{MEMBER_FIELDS.filter(
(f) => mapping[f.key] !== undefined,
).map((f) => (

View File

@@ -200,12 +200,24 @@ export function MembersDataTable({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="px-4 py-3 text-left font-medium">Nr</th>
<th className="px-4 py-3 text-left font-medium">Name</th>
<th className="px-4 py-3 text-left font-medium">E-Mail</th>
<th className="px-4 py-3 text-left font-medium">Ort</th>
<th className="px-4 py-3 text-left font-medium">Status</th>
<th className="px-4 py-3 text-left font-medium">Eintritt</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Nr
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Name
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
E-Mail
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Ort
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Status
</th>
<th scope="col" className="px-4 py-3 text-left font-medium">
Eintritt
</th>
</tr>
</thead>
<tbody>

View File

@@ -22,6 +22,9 @@
"clean": "git clean -xdf .turbo node_modules",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next-intl": "catalog:"
},
"devDependencies": {
"@kit/next": "workspace:*",
"@kit/shared": "workspace:*",
@@ -31,7 +34,6 @@
"@supabase/supabase-js": "catalog:",
"@types/react": "catalog:",
"next": "catalog:",
"next-intl": "catalog:",
"next-safe-action": "catalog:",
"react": "catalog:",
"react-hook-form": "catalog:",

View File

@@ -98,7 +98,7 @@ export function ModuleTable({
<thead>
<tr className="bg-muted/50 border-b">
{onSelectionChange && (
<th className="w-10 p-3">
<th scope="col" className="w-10 p-3">
<input
type="checkbox"
checked={

View File

@@ -42,9 +42,9 @@ export function createFileService(client: SupabaseClient<Database>) {
.from('cms_files')
.insert({
account_id: input.accountId,
record_id: input.recordId ?? null,
module_name: input.moduleName ?? null,
field_name: input.fieldName ?? null,
module_name: input.moduleName ?? '',
field_name: input.fieldName ?? '',
record_id: input.recordId ?? '',
file_name: input.file.name,
original_name: input.file.name,
mime_type: input.file.type,
@@ -89,20 +89,18 @@ export function createFileService(client: SupabaseClient<Database>) {
},
async deleteFile(fileId: string) {
const numId = Number(fileId);
const { data: file, error: getErr } = await client
.from('cms_files')
.select('storage_path')
.eq('id', fileId)
.eq('id', numId)
.single();
if (getErr) throw getErr;
await client.storage.from('cms-files').remove([file.storage_path]);
const { error } = await client
.from('cms_files')
.delete()
.eq('id', fileId);
const { error } = await client.from('cms_files').delete().eq('id', numId);
if (error) throw error;
},

View File

@@ -107,7 +107,10 @@ export function createNewsletterApi(client: SupabaseClient<Database>) {
query = query.ilike('subject', `%${opts.search}%`);
}
if (opts?.status) {
query = query.eq('status', opts.status);
query = query.eq(
'status',
opts.status as Database['public']['Enums']['newsletter_status'],
);
}
query = query.range((page - 1) * pageSize, page * pageSize - 1);

View File

@@ -3,4 +3,4 @@ export { MeetingsDashboard } from './meetings-dashboard';
export { ProtocolsDataTable } from './protocols-data-table';
export { CreateProtocolForm } from './create-protocol-form';
export { ProtocolItemsList } from './protocol-items-list';
export { OpenTasksView } from './open-tasks-view';
export { OpenTasksView, type OpenTask } from './open-tasks-view';

View File

@@ -21,8 +21,7 @@ interface RecentProtocol {
id: string;
title: string;
meeting_date: string;
meeting_type: string;
is_published: boolean;
status: string;
}
interface OverdueTask {
@@ -163,11 +162,11 @@ export function MeetingsDashboard({
<p className="text-muted-foreground text-xs">
{formatDate(protocol.meeting_date)}
{' · '}
{MEETING_TYPE_LABELS[protocol.meeting_type] ??
protocol.meeting_type}
{MEETING_TYPE_LABELS[protocol.status] ??
protocol.status}
</p>
</div>
{protocol.is_published && (
{protocol.status === 'final' && (
<Badge variant="default" className="ml-2 shrink-0">
Veröffentlicht
</Badge>

View File

@@ -14,10 +14,10 @@ import {
ITEM_STATUS_COLORS,
} from '../lib/meetings-constants';
interface OpenTask {
export interface OpenTask {
id: string;
title: string;
description: string | null;
content: string | null;
responsible_person: string | null;
due_date: string | null;
status: string;
@@ -25,7 +25,6 @@ interface OpenTask {
id: string;
title: string;
meeting_date: string;
meeting_type: string;
};
}
@@ -92,11 +91,21 @@ export function OpenTasksView({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Aufgabe</th>
<th className="p-3 text-left font-medium">Protokoll</th>
<th className="p-3 text-left font-medium">Zuständig</th>
<th className="p-3 text-left font-medium">Fällig</th>
<th className="p-3 text-center font-medium">Status</th>
<th scope="col" className="p-3 text-left font-medium">
Aufgabe
</th>
<th scope="col" className="p-3 text-left font-medium">
Protokoll
</th>
<th scope="col" className="p-3 text-left font-medium">
Zuständig
</th>
<th scope="col" className="p-3 text-left font-medium">
Fällig
</th>
<th scope="col" className="p-3 text-center font-medium">
Status
</th>
</tr>
</thead>
<tbody>
@@ -117,9 +126,9 @@ export function OpenTasksView({
<td className="p-3">
<div>
<p className="font-medium">{task.title}</p>
{task.description && (
{task.content && (
<p className="text-muted-foreground mt-0.5 line-clamp-1 text-xs">
{task.description}
{task.content}
</p>
)}
</div>

View File

@@ -21,7 +21,7 @@ import {
interface ProtocolItem {
id: string;
title: string;
description: string | null;
content: string | null;
responsible_person: string | null;
due_date: string | null;
status: string;
@@ -103,12 +103,24 @@ export function ProtocolItemsList({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">#</th>
<th className="p-3 text-left font-medium">Titel</th>
<th className="p-3 text-left font-medium">Zuständig</th>
<th className="p-3 text-left font-medium">Fällig</th>
<th className="p-3 text-center font-medium">Status</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
#
</th>
<th scope="col" className="p-3 text-left font-medium">
Titel
</th>
<th scope="col" className="p-3 text-left font-medium">
Zuständig
</th>
<th scope="col" className="p-3 text-left font-medium">
Fällig
</th>
<th scope="col" className="p-3 text-center font-medium">
Status
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -124,9 +136,9 @@ export function ProtocolItemsList({
<td className="p-3">
<div>
<p className="font-medium">{item.title}</p>
{item.description && (
{item.content && (
<p className="text-muted-foreground mt-0.5 line-clamp-1 text-xs">
{item.description}
{item.content}
</p>
)}
</div>

View File

@@ -160,11 +160,21 @@ export function ProtocolsDataTable({
<table className="w-full text-sm">
<thead>
<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">Titel</th>
<th className="p-3 text-left font-medium">Sitzungsart</th>
<th className="p-3 text-left font-medium">Ort</th>
<th className="p-3 text-center font-medium">Status</th>
<th scope="col" className="p-3 text-left font-medium">
Datum
</th>
<th scope="col" className="p-3 text-left font-medium">
Titel
</th>
<th scope="col" className="p-3 text-left font-medium">
Sitzungsart
</th>
<th scope="col" className="p-3 text-left font-medium">
Ort
</th>
<th scope="col" className="p-3 text-center font-medium">
Status
</th>
</tr>
</thead>
<tbody>

View File

@@ -33,7 +33,7 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
let query = client
.from('meeting_protocols')
.select(
'id, title, meeting_date, meeting_type, location, attendees, remarks, is_published, created_at, updated_at',
'id, title, meeting_date, status, location, attendees, summary, created_at, updated_at',
{ count: 'exact' },
)
.eq('account_id', accountId)
@@ -46,7 +46,7 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
}
if (opts?.meetingType) {
query = query.eq('meeting_type', opts.meetingType);
query = query.eq('status', opts.meetingType);
}
if (opts?.year) {
@@ -80,12 +80,11 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
.insert({
account_id: input.accountId,
title: input.title,
meeting_date: input.meetingDate || null,
meeting_type: input.meetingType,
location: input.location || null,
meeting_date: input.meetingDate,
location: input.location ?? null,
attendees: input.attendees,
remarks: input.remarks,
is_published: input.isPublished,
summary: input.remarks ?? null,
status: input.isPublished ? 'final' : 'entwurf',
created_by: userId,
updated_by: userId,
})
@@ -101,13 +100,11 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
if (input.title !== undefined) updateData.title = input.title;
if (input.meetingDate !== undefined)
updateData.meeting_date = input.meetingDate;
if (input.meetingType !== undefined)
updateData.meeting_type = input.meetingType;
if (input.location !== undefined) updateData.location = input.location;
if (input.attendees !== undefined) updateData.attendees = input.attendees;
if (input.remarks !== undefined) updateData.remarks = input.remarks;
if (input.remarks !== undefined) updateData.summary = input.remarks;
if (input.isPublished !== undefined)
updateData.is_published = input.isPublished;
updateData.status = input.isPublished ? 'final' : 'entwurf';
const { data, error } = await client
.from('meeting_protocols')
@@ -135,14 +132,17 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
let query = client
.from('meeting_protocol_items')
.select(
'id, protocol_id, title, description, responsible_person, due_date, status, sort_order, created_at, updated_at',
'id, protocol_id, title, content, responsible_person, due_date, status, sort_order, item_type, item_number, decision_text, created_at, updated_at',
)
.eq('protocol_id', protocolId)
.order('sort_order')
.order('created_at');
if (opts?.status) {
query = query.eq('status', opts.status);
query = query.eq(
'status',
opts.status as Database['public']['Enums']['meeting_item_status'],
);
}
const { data, error } = await query;
@@ -156,13 +156,11 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
.insert({
protocol_id: input.protocolId,
title: input.title,
description: input.description,
responsible_person: input.responsiblePerson,
due_date: input.dueDate,
content: input.description ?? null,
responsible_person: input.responsiblePerson ?? null,
due_date: input.dueDate ?? null,
status: input.status,
sort_order: input.sortOrder,
created_by: userId,
updated_by: userId,
sort_order: input.sortOrder ?? 0,
})
.select()
.single();
@@ -171,11 +169,11 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
},
async updateItem(input: UpdateProtocolItemInput, userId: string) {
const updateData: Record<string, unknown> = { updated_by: userId };
const updateData: Record<string, unknown> = {};
if (input.title !== undefined) updateData.title = input.title;
if (input.description !== undefined)
updateData.description = input.description;
updateData.content = input.description;
if (input.responsiblePerson !== undefined)
updateData.responsible_person = input.responsiblePerson;
if (input.dueDate !== undefined) updateData.due_date = input.dueDate;
@@ -198,7 +196,6 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
.from('meeting_protocol_items')
.update({
status: input.status,
updated_by: userId,
})
.eq('id', input.itemId)
.select()
@@ -242,7 +239,7 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
const { data, error, count } = await client
.from('meeting_protocol_items')
.select(
'id, protocol_id, title, description, responsible_person, due_date, status, sort_order, created_at, updated_at, meeting_protocols!inner ( id, title, meeting_date, meeting_type, account_id )',
'id, protocol_id, title, content, responsible_person, due_date, status, sort_order, created_at, updated_at, meeting_protocols!inner ( id, title, meeting_date, account_id )',
{ count: 'exact' },
)
.eq('meeting_protocols.account_id', accountId)
@@ -286,7 +283,7 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
file_name: fileName,
file_path: filePath,
file_size: fileSize,
mime_type: mimeType,
content_type: mimeType,
created_by: userId,
})
.select()
@@ -364,7 +361,7 @@ export function createMeetingsApi(client: SupabaseClient<Database>) {
async getRecentProtocols(accountId: string, limit = 5) {
const { data, error } = await client
.from('meeting_protocols')
.select('id, title, meeting_date, meeting_type, is_published')
.select('id, title, meeting_date, status')
.eq('account_id', accountId)
.order('meeting_date', { ascending: false })
.limit(limit);

View File

@@ -276,11 +276,21 @@ export function ClubContactsManager({
<table className="w-full text-sm">
<thead>
<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">Funktion</th>
<th className="p-3 text-left font-medium">Telefon</th>
<th className="p-3 text-left font-medium">E-Mail</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Funktion
</th>
<th scope="col" className="p-3 text-left font-medium">
Telefon
</th>
<th scope="col" className="p-3 text-left font-medium">
E-Mail
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -288,7 +298,7 @@ export function ClubContactsManager({
<tr key={String(contact.id)} className="border-b">
<td className="p-3 font-medium">
{String(contact.first_name)} {String(contact.last_name)}
{contact.is_primary && (
{Boolean(contact.is_active) && (
<Star className="ml-1 inline h-3 w-3 fill-amber-400 text-amber-400" />
)}
</td>
@@ -310,19 +320,24 @@ export function ClubContactsManager({
data-test="contact-edit-btn"
variant="ghost"
size="sm"
aria-label="Kontakt bearbeiten"
onClick={() => handleEdit(contact)}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-4 w-4" aria-hidden="true" />
</Button>
<Button
data-test="contact-delete-btn"
variant="ghost"
size="sm"
aria-label="Kontakt löschen"
onClick={() =>
executeDelete({ contactId: String(contact.id) })
}
>
<Trash2 className="text-destructive h-4 w-4" />
<Trash2
className="text-destructive h-4 w-4"
aria-hidden="true"
/>
</Button>
</div>
</td>

View File

@@ -80,13 +80,27 @@ export function ClubFeeBillingTable({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Beitragsart</th>
<th className="p-3 text-center font-medium">Jahr</th>
<th className="p-3 text-right font-medium">Betrag</th>
<th className="p-3 text-left font-medium">Fällig</th>
<th className="p-3 text-left font-medium">Status</th>
<th className="p-3 text-left font-medium">Zahlung</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Beitragsart
</th>
<th scope="col" className="p-3 text-center font-medium">
Jahr
</th>
<th scope="col" className="p-3 text-right font-medium">
Betrag
</th>
<th scope="col" className="p-3 text-left font-medium">
Fällig
</th>
<th scope="col" className="p-3 text-left font-medium">
Status
</th>
<th scope="col" className="p-3 text-left font-medium">
Zahlung
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr>
</thead>
<tbody>
@@ -102,13 +116,17 @@ export function ClubFeeBillingTable({
{feeTypeName ? String(feeTypeName) : '—'}
</td>
<td className="p-3 text-center">
{String(billing.year)}
{String(billing.billing_year ?? billing.year ?? '—')}
</td>
<td className="p-3 text-right">
{formatCurrencyAmount(billing.amount)}
{formatCurrencyAmount(billing.amount as number)}
</td>
<td className="text-muted-foreground p-3">
{formatDate(billing.due_date)}
{formatDate(
(billing.invoice_date ?? billing.due_date) as
| string
| null,
)}
</td>
<td className="p-3">
<Badge

View File

@@ -90,16 +90,16 @@ export function ClubNotesList({ notes, clubId }: ClubNotesListProps) {
<div className="flex items-center gap-2">
<span className="font-medium">{String(note.title)}</span>
<Badge variant="secondary" className="gap-1">
{NOTE_ICONS[noteType]}
{NOTE_ICONS[noteType] ?? null}
{NOTE_TYPE_LABELS[noteType] ?? noteType}
</Badge>
{note.due_date && (
{Boolean(note.note_date) && (
<span className="text-muted-foreground text-xs">
Fällig: {formatDate(note.due_date)}
{formatDate(note.note_date as string)}
</span>
)}
</div>
{note.content && (
{Boolean(note.content) && (
<p className="text-muted-foreground text-sm">
{String(note.content)}
</p>
@@ -108,9 +108,13 @@ export function ClubNotesList({ notes, clubId }: ClubNotesListProps) {
<Button
variant="ghost"
size="sm"
aria-label="Notiz löschen"
onClick={() => executeDelete({ noteId: String(note.id) })}
>
<Trash2 className="text-destructive h-4 w-4" />
<Trash2
className="text-destructive h-4 w-4"
aria-hidden="true"
/>
</Button>
</div>
);
@@ -135,9 +139,13 @@ export function ClubNotesList({ notes, clubId }: ClubNotesListProps) {
<Button
variant="ghost"
size="sm"
aria-label="Notiz löschen"
onClick={() => executeDelete({ noteId: String(note.id) })}
>
<Trash2 className="text-destructive h-4 w-4" />
<Trash2
className="text-destructive h-4 w-4"
aria-hidden="true"
/>
</Button>
</div>
))}

View File

@@ -165,11 +165,21 @@ export function ClubsDataTable({
<table className="w-full text-sm">
<thead>
<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">Typ</th>
<th className="p-3 text-right font-medium">Mitglieder</th>
<th className="p-3 text-left font-medium">Ort</th>
<th className="p-3 text-left font-medium">Kontakt</th>
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Typ
</th>
<th scope="col" className="p-3 text-right font-medium">
Mitglieder
</th>
<th scope="col" className="p-3 text-left font-medium">
Ort
</th>
<th scope="col" className="p-3 text-left font-medium">
Kontakt
</th>
</tr>
</thead>
<tbody>
@@ -194,7 +204,7 @@ export function ClubsDataTable({
>
{String(club.name)}
</Link>
{club.is_archived && (
{Boolean(club.is_archived) && (
<Badge variant="secondary" className="ml-2">
Archiviert
</Badge>
@@ -210,11 +220,11 @@ export function ClubsDataTable({
)}
</td>
<td className="p-3 text-right">
{formatNumber(club.member_count)}
{formatNumber(club.member_count as number)}
</td>
<td className="text-muted-foreground p-3">
{club.city
? `${String(club.zip ?? '')} ${String(club.city)}`.trim()
{club.address_city
? `${String(club.address_zip ?? '')} ${String(club.address_city)}`.trim()
: '—'}
</td>
<td className="text-muted-foreground p-3">

View File

@@ -253,13 +253,27 @@ export function CrossOrgMemberSearch({
<table className="w-full text-sm">
<thead>
<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">Organisation</th>
<th className="p-3 text-left font-medium">E-Mail</th>
<th className="p-3 text-left font-medium">Ort</th>
<th className="p-3 text-center font-medium">Status</th>
<th className="p-3 text-left font-medium">Eintritt</th>
<th className="p-3 text-right font-medium">Aktion</th>
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Organisation
</th>
<th scope="col" className="p-3 text-left font-medium">
E-Mail
</th>
<th scope="col" className="p-3 text-left font-medium">
Ort
</th>
<th scope="col" className="p-3 text-center font-medium">
Status
</th>
<th scope="col" className="p-3 text-left font-medium">
Eintritt
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktion
</th>
</tr>
</thead>
<tbody>

View File

@@ -201,14 +201,30 @@ export function HierarchyEvents({
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Veranstaltung</th>
<th className="p-3 text-left font-medium">Organisation</th>
<th className="p-3 text-left font-medium">Datum</th>
<th className="p-3 text-left font-medium">Ort</th>
<th className="p-3 text-center font-medium">Kapazität</th>
<th className="p-3 text-right font-medium">Gebühr</th>
<th className="p-3 text-center font-medium">Status</th>
<th className="p-3 text-center font-medium">Geteilt</th>
<th scope="col" className="p-3 text-left font-medium">
Veranstaltung
</th>
<th scope="col" className="p-3 text-left font-medium">
Organisation
</th>
<th scope="col" className="p-3 text-left font-medium">
Datum
</th>
<th scope="col" className="p-3 text-left font-medium">
Ort
</th>
<th scope="col" className="p-3 text-center font-medium">
Kapazität
</th>
<th scope="col" className="p-3 text-right font-medium">
Gebühr
</th>
<th scope="col" className="p-3 text-center font-medium">
Status
</th>
<th scope="col" className="p-3 text-center font-medium">
Geteilt
</th>
</tr>
</thead>
<tbody>

View File

@@ -204,19 +204,31 @@ export function HierarchyReport({ summary, report }: HierarchyReportProps) {
<table className="w-full text-sm">
<thead>
<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">Ebene</th>
<th className="p-3 text-right font-medium">
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
Ebene
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktive Mitgl.
</th>
<th className="p-3 text-right font-medium">Gesamt</th>
<th className="p-3 text-right font-medium">Neu (Jahr)</th>
<th className="p-3 text-right font-medium">Kurse</th>
<th className="p-3 text-right font-medium">Termine</th>
<th className="p-3 text-right font-medium">
<th scope="col" className="p-3 text-right font-medium">
Gesamt
</th>
<th scope="col" className="p-3 text-right font-medium">
Neu (Jahr)
</th>
<th scope="col" className="p-3 text-right font-medium">
Kurse
</th>
<th scope="col" className="p-3 text-right font-medium">
Termine
</th>
<th scope="col" className="p-3 text-right font-medium">
Offene Rechn.
</th>
<th className="p-3 text-right font-medium">
<th scope="col" className="p-3 text-right font-medium">
Offener Betrag
</th>
</tr>

View File

@@ -144,12 +144,24 @@ export function SharedTemplates({
<table className="w-full text-sm">
<thead>
<tr className="text-muted-foreground border-b text-left">
<th className="pr-4 pb-2 font-medium">Name</th>
<th className="pr-4 pb-2 font-medium">Typ</th>
<th className="pr-4 pb-2 font-medium">Template-Typ</th>
<th className="pr-4 pb-2 font-medium">Organisation</th>
<th className="pr-4 pb-2 font-medium">Erstellt</th>
<th className="pb-2 font-medium">Aktion</th>
<th scope="col" className="pr-4 pb-2 font-medium">
Name
</th>
<th scope="col" className="pr-4 pb-2 font-medium">
Typ
</th>
<th scope="col" className="pr-4 pb-2 font-medium">
Template-Typ
</th>
<th scope="col" className="pr-4 pb-2 font-medium">
Organisation
</th>
<th scope="col" className="pr-4 pb-2 font-medium">
Erstellt
</th>
<th scope="col" className="pb-2 font-medium">
Aktion
</th>
</tr>
</thead>
<tbody>

View File

@@ -487,7 +487,14 @@ export const upsertAssociationHistory = authActionClient
{ name: 'verband.history.upsert' },
'Upserting association history...',
);
const result = await api.upsertHistory(input);
// Look up the account_id from the club
const supabase = getSupabaseServerClient();
const { data: club } = await supabase
.from('member_clubs')
.select('account_id')
.eq('id', input.clubId)
.single();
const result = await api.upsertHistory(input, club?.account_id ?? '');
logger.info(
{ name: 'verband.history.upsert' },
'Association history upserted',

View File

@@ -55,7 +55,7 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
let query = client
.from('member_clubs')
.select(
'id, name, short_name, association_type_id, member_count, founded_year, street, zip, city, phone, email, website, iban, bic, account_holder, is_archived, created_at, updated_at, association_types ( id, name )',
'id, name, short_name, association_type_id, member_count, address_street, address_zip, address_city, phone, email, website, iban, bic, account_holder, is_archived, created_at, updated_at, association_types ( id, name )',
{ count: 'exact' },
)
.eq('account_id', accountId)
@@ -63,7 +63,7 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
if (opts?.search) {
query = query.or(
`name.ilike.%${opts.search}%,short_name.ilike.%${opts.search}%,city.ilike.%${opts.search}%`,
`name.ilike.%${opts.search}%,short_name.ilike.%${opts.search}%,address_city.ilike.%${opts.search}%`,
);
}
@@ -105,10 +105,9 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
short_name: input.shortName || null,
association_type_id: input.associationTypeId,
member_count: input.memberCount,
founded_year: input.foundedYear,
street: input.street,
zip: input.zip,
city: input.city,
address_street: input.street ?? null,
address_zip: input.zip ?? null,
address_city: input.city ?? null,
phone: input.phone || null,
email: input.email || null,
website: input.website || null,
@@ -135,11 +134,9 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
updateData.association_type_id = input.associationTypeId;
if (input.memberCount !== undefined)
updateData.member_count = input.memberCount;
if (input.foundedYear !== undefined)
updateData.founded_year = input.foundedYear;
if (input.street !== undefined) updateData.street = input.street;
if (input.zip !== undefined) updateData.zip = input.zip;
if (input.city !== undefined) updateData.city = input.city;
if (input.street !== undefined) updateData.address_street = input.street;
if (input.zip !== undefined) updateData.address_zip = input.zip;
if (input.city !== undefined) updateData.address_city = input.city;
if (input.phone !== undefined) updateData.phone = input.phone;
if (input.email !== undefined) updateData.email = input.email;
if (input.website !== undefined) updateData.website = input.website;
@@ -184,32 +181,31 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
client
.from('club_contacts')
.select(
'id, club_id, first_name, last_name, role, phone, email, is_primary, created_at',
'id, club_id, first_name, last_name, role_id, phone, email, is_active, created_at',
)
.eq('club_id', clubId)
.order('is_primary', { ascending: false })
.order('last_name'),
client
.from('club_fee_billings')
.select(
'id, club_id, fee_type_id, year, amount, due_date, paid_date, payment_method, status, notes, created_at, club_fee_types ( id, name )',
'id, club_id, fee_type_id, billing_year, amount, invoice_date, paid_date, status, remarks, created_at',
)
.eq('club_id', clubId)
.order('year', { ascending: false })
.order('billing_year', { ascending: false })
.limit(50),
client
.from('club_notes')
.select(
'id, club_id, title, content, note_type, due_date, is_completed, created_at, updated_at',
)
.select('id, club_id, title, content, note_date, created_at')
.eq('club_id', clubId)
.order('created_at', { ascending: false })
.limit(50),
client
.from('association_history')
.select('id, club_id, year, member_count, notes, created_at')
.select(
'id, club_id, event_date, event_type, description, created_at',
)
.eq('club_id', clubId)
.order('year', { ascending: false }),
.order('event_date', { ascending: false }),
]);
if (clubResult.error) throw clubResult.error;
@@ -231,10 +227,9 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
const { data, error } = await client
.from('club_contacts')
.select(
'id, club_id, first_name, last_name, role, phone, email, is_primary, created_at',
'id, club_id, first_name, last_name, role_id, phone, email, is_active, created_at',
)
.eq('club_id', clubId)
.order('is_primary', { ascending: false })
.order('last_name');
if (error) throw error;
return data ?? [];
@@ -247,10 +242,8 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
club_id: input.clubId,
first_name: input.firstName,
last_name: input.lastName,
role: input.role,
phone: input.phone || null,
email: input.email || null,
is_primary: input.isPrimary,
phone: input.phone ?? null,
email: input.email ?? null,
})
.select()
.single();
@@ -264,11 +257,8 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
if (input.firstName !== undefined)
updateData.first_name = input.firstName;
if (input.lastName !== undefined) updateData.last_name = input.lastName;
if (input.role !== undefined) updateData.role = input.role;
if (input.phone !== undefined) updateData.phone = input.phone;
if (input.email !== undefined) updateData.email = input.email;
if (input.isPrimary !== undefined)
updateData.is_primary = input.isPrimary;
const { data, error } = await client
.from('club_contacts')
@@ -489,14 +479,14 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
let query = client
.from('club_fee_billings')
.select(
'id, club_id, fee_type_id, year, amount, due_date, paid_date, payment_method, status, notes, created_at, club_fee_types ( id, name )',
'id, club_id, fee_type_id, billing_year, amount, invoice_date, paid_date, status, remarks, created_at',
{ count: 'exact' },
)
.eq('club_id', clubId)
.order('year', { ascending: false });
.order('billing_year', { ascending: false });
if (opts?.year) {
query = query.eq('year', opts.year);
query = query.eq('billing_year', opts.year);
}
if (opts?.status) {
query = query.eq('status', opts.status);
@@ -515,11 +505,11 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
const { data, error } = await client
.from('club_fee_billings')
.select(
'id, club_id, fee_type_id, year, amount, due_date, status, member_clubs ( id, name ), club_fee_types ( id, name )',
'id, club_id, fee_type_id, billing_year, amount, invoice_date, paid_date, status, remarks, created_at, member_clubs ( id, name )',
)
.eq('member_clubs.account_id', accountId)
.in('status', ['offen', 'ueberfaellig'])
.order('due_date');
.order('invoice_date');
if (error) throw error;
return data ?? [];
},
@@ -530,13 +520,12 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
.insert({
club_id: input.clubId,
fee_type_id: input.feeTypeId,
year: input.year,
billing_year: input.year,
amount: input.amount,
due_date: input.dueDate || null,
paid_date: input.paidDate || null,
payment_method: input.paymentMethod,
invoice_date: input.dueDate ?? null,
paid_date: input.paidDate ?? null,
status: input.status,
notes: input.notes,
remarks: input.notes ?? null,
})
.select()
.single();
@@ -552,15 +541,14 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
if (updates.feeTypeId !== undefined)
updateData.fee_type_id = updates.feeTypeId;
if (updates.year !== undefined) updateData.year = updates.year;
if (updates.year !== undefined) updateData.billing_year = updates.year;
if (updates.amount !== undefined) updateData.amount = updates.amount;
if (updates.dueDate !== undefined) updateData.due_date = updates.dueDate;
if (updates.dueDate !== undefined)
updateData.invoice_date = updates.dueDate;
if (updates.paidDate !== undefined)
updateData.paid_date = updates.paidDate;
if (updates.paymentMethod !== undefined)
updateData.payment_method = updates.paymentMethod;
if (updates.status !== undefined) updateData.status = updates.status;
if (updates.notes !== undefined) updateData.notes = updates.notes;
if (updates.notes !== undefined) updateData.remarks = updates.notes;
const { data, error } = await client
.from('club_fee_billings')
@@ -587,11 +575,8 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
async listNotes(clubId: string) {
const { data, error } = await client
.from('club_notes')
.select(
'id, club_id, title, content, note_type, due_date, is_completed, created_at, updated_at',
)
.select('id, club_id, title, content, note_date, created_at')
.eq('club_id', clubId)
.order('is_completed')
.order('created_at', { ascending: false });
if (error) throw error;
return data ?? [];
@@ -602,11 +587,9 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
.from('club_notes')
.insert({
club_id: input.clubId,
title: input.title,
content: input.content,
note_type: input.noteType,
due_date: input.dueDate || null,
is_completed: input.isCompleted,
title: input.title ?? null,
content: input.content ?? '',
note_date: input.dueDate ?? new Date().toISOString().split('T')[0]!,
})
.select()
.single();
@@ -619,11 +602,7 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
if (updates.title !== undefined) updateData.title = updates.title;
if (updates.content !== undefined) updateData.content = updates.content;
if (updates.noteType !== undefined)
updateData.note_type = updates.noteType;
if (updates.dueDate !== undefined) updateData.due_date = updates.dueDate;
if (updates.isCompleted !== undefined)
updateData.is_completed = updates.isCompleted;
if (updates.dueDate !== undefined) updateData.note_date = updates.dueDate;
const { data, error } = await client
.from('club_notes')
@@ -636,14 +615,12 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
},
async completeNote(noteId: string) {
const { data, error } = await client
// club_notes has no is_completed flag — delete the note to mark it done
const { error } = await client
.from('club_notes')
.update({ is_completed: true })
.eq('id', noteId)
.select()
.single();
.delete()
.eq('id', noteId);
if (error) throw error;
return data;
},
async deleteNote(noteId: string) {
@@ -661,25 +638,28 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
async listHistory(clubId: string) {
const { data, error } = await client
.from('association_history')
.select('id, club_id, year, member_count, notes, created_at')
.select('id, club_id, event_date, event_type, description, created_at')
.eq('club_id', clubId)
.order('year', { ascending: false });
.order('event_date', { ascending: false });
if (error) throw error;
return data ?? [];
},
async upsertHistory(input: CreateAssociationHistoryInput) {
async upsertHistory(
input: CreateAssociationHistoryInput,
accountId: string,
) {
const { data, error } = await client
.from('association_history')
.upsert(
{
club_id: input.clubId,
year: input.year,
member_count: input.memberCount,
notes: input.notes,
},
{ onConflict: 'club_id,year' },
)
.insert({
club_id: input.clubId,
account_id: accountId,
event_type: 'manual',
description: input.notes ?? '',
event_date: input.year
? `${input.year}-01-01`
: new Date().toISOString().split('T')[0]!,
})
.select()
.single();
if (error) throw error;
@@ -717,10 +697,7 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
.from('club_fee_billings')
.select('id, amount', { count: 'exact' })
.in('status', ['offen', 'ueberfaellig']),
client
.from('club_notes')
.select('id', { count: 'exact', head: true })
.eq('is_completed', false),
client.from('club_notes').select('id', { count: 'exact', head: true }),
client
.from('member_clubs')
.select('member_count')
@@ -1023,7 +1000,7 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
const { data: newsletters } = await client
.from('newsletter_recipients')
.select(
'id, newsletter_id, newsletters(id, name, account_id, accounts(name))',
'id, newsletter_id, newsletters(id, subject, account_id, accounts(name))',
)
.eq('member_id', memberId);
@@ -1079,7 +1056,7 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
> | null;
return {
id: String(recipientRecord.id),
name: String(newsletterData?.name ?? '—'),
name: String(newsletterData?.subject ?? '—'),
accountName: String(newsletterAccountData?.name ?? '—'),
survives: true, // FK on member_id, stays linked
};
@@ -1124,14 +1101,17 @@ export function createVerbandApi(client: SupabaseClient<Database>) {
},
async getMemberTransferHistory(memberId: string) {
const { data, error } = await client
.from('member_transfers' as string)
// member_transfers table may not be in generated types yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query = (client as any)
.from('member_transfers')
.select(
'id, member_id, source_account_id, target_account_id, reason, transferred_at' as '*',
'id, member_id, source_account_id, target_account_id, reason, transferred_at',
)
.eq('member_id', memberId)
.order('transferred_at', { ascending: false });
const { data, error } = await query;
if (error) throw error;
return (data ?? []) as unknown as MemberTransfer[];
},