feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Major changes: - Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI - Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions - Sitzungsprotokolle module: meeting protocols, agenda items, task tracking - Verbandsverwaltung module: federation management, member clubs, contacts, fees - Per-account module activation via Modules page toggle - Site Builder: live CMS data in Puck blocks (courses, events, membership registration) - Public registration APIs: course signup, event registration, membership application - Document generation: PDF member cards, Excel reports, HTML labels - Landing page: real Com.BISS content (no filler text) - UX audit fixes: AccountNotFound component, shared status badges, confirm dialog, pagination, duplicate heading removal, emoji→badge replacement, a11y fixes - QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import {
|
||||
CreateMemberClubSchema,
|
||||
UpdateMemberClubSchema,
|
||||
CreateClubContactSchema,
|
||||
UpdateClubContactSchema,
|
||||
CreateClubRoleSchema,
|
||||
CreateAssociationTypeSchema,
|
||||
CreateClubFeeTypeSchema,
|
||||
CreateClubFeeBillingSchema,
|
||||
CreateClubNoteSchema,
|
||||
CreateAssociationHistorySchema,
|
||||
} from '../../schema/verband.schema';
|
||||
|
||||
import { createVerbandApi } from '../api';
|
||||
|
||||
const REVALIDATE_PATH = '/home/[account]/verband';
|
||||
|
||||
// =====================================================
|
||||
// Clubs (Vereine)
|
||||
// =====================================================
|
||||
|
||||
export const createClub = authActionClient
|
||||
.inputSchema(CreateMemberClubSchema)
|
||||
.action(async ({ parsedInput: input, ctx }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'verband.club.create' }, 'Creating club...');
|
||||
const result = await api.createClub(input, userId);
|
||||
logger.info({ name: 'verband.club.create' }, 'Club created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateClub = authActionClient
|
||||
.inputSchema(UpdateMemberClubSchema)
|
||||
.action(async ({ parsedInput: input, ctx }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
const userId = ctx.user.id;
|
||||
|
||||
logger.info({ name: 'verband.club.update' }, 'Updating club...');
|
||||
const result = await api.updateClub(input, userId);
|
||||
logger.info({ name: 'verband.club.update' }, 'Club updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const archiveClub = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
clubId: z.string().uuid(),
|
||||
accountId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.club.archive' }, 'Archiving club...');
|
||||
await api.archiveClub(input.clubId);
|
||||
logger.info({ name: 'verband.club.archive' }, 'Club archived');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Contacts (Ansprechpartner)
|
||||
// =====================================================
|
||||
|
||||
export const createContact = authActionClient
|
||||
.inputSchema(CreateClubContactSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.contact.create' }, 'Creating contact...');
|
||||
const result = await api.createContact(input);
|
||||
logger.info({ name: 'verband.contact.create' }, 'Contact created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateContact = authActionClient
|
||||
.inputSchema(UpdateClubContactSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.contact.update' }, 'Updating contact...');
|
||||
const result = await api.updateContact(input);
|
||||
logger.info({ name: 'verband.contact.update' }, 'Contact updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const deleteContact = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
contactId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.contact.delete' }, 'Deleting contact...');
|
||||
await api.deleteContact(input.contactId);
|
||||
logger.info({ name: 'verband.contact.delete' }, 'Contact deleted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Roles (Funktionen)
|
||||
// =====================================================
|
||||
|
||||
export const createRole = authActionClient
|
||||
.inputSchema(CreateClubRoleSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.role.create' }, 'Creating role...');
|
||||
const result = await api.createRole(input);
|
||||
logger.info({ name: 'verband.role.create' }, 'Role created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateRole = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
roleId: z.string().uuid(),
|
||||
name: z.string().min(1).max(128).optional(),
|
||||
description: z.string().max(512).optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
const { roleId, ...updates } = input;
|
||||
logger.info({ name: 'verband.role.update' }, 'Updating role...');
|
||||
const result = await api.updateRole(roleId, updates);
|
||||
logger.info({ name: 'verband.role.update' }, 'Role updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const deleteRole = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
roleId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.role.delete' }, 'Deleting role...');
|
||||
await api.deleteRole(input.roleId);
|
||||
logger.info({ name: 'verband.role.delete' }, 'Role deleted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Association Types (Vereinstypen)
|
||||
// =====================================================
|
||||
|
||||
export const createAssociationType = authActionClient
|
||||
.inputSchema(CreateAssociationTypeSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.type.create' }, 'Creating association type...');
|
||||
const result = await api.createType(input);
|
||||
logger.info({ name: 'verband.type.create' }, 'Association type created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateAssociationType = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
typeId: z.string().uuid(),
|
||||
name: z.string().min(1).max(128).optional(),
|
||||
description: z.string().max(512).optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
const { typeId, ...updates } = input;
|
||||
logger.info({ name: 'verband.type.update' }, 'Updating association type...');
|
||||
const result = await api.updateType(typeId, updates);
|
||||
logger.info({ name: 'verband.type.update' }, 'Association type updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const deleteAssociationType = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
typeId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.type.delete' }, 'Deleting association type...');
|
||||
await api.deleteType(input.typeId);
|
||||
logger.info({ name: 'verband.type.delete' }, 'Association type deleted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Fee Types (Beitragsarten)
|
||||
// =====================================================
|
||||
|
||||
export const createFeeType = authActionClient
|
||||
.inputSchema(CreateClubFeeTypeSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.feeType.create' }, 'Creating fee type...');
|
||||
const result = await api.createFeeType(input);
|
||||
logger.info({ name: 'verband.feeType.create' }, 'Fee type created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateFeeType = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
feeTypeId: z.string().uuid(),
|
||||
name: z.string().min(1).max(128).optional(),
|
||||
description: z.string().max(512).optional(),
|
||||
defaultAmount: z.number().min(0).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
const { feeTypeId, ...updates } = input;
|
||||
logger.info({ name: 'verband.feeType.update' }, 'Updating fee type...');
|
||||
const result = await api.updateFeeType(feeTypeId, updates);
|
||||
logger.info({ name: 'verband.feeType.update' }, 'Fee type updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const deleteFeeType = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
feeTypeId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.feeType.delete' }, 'Deleting fee type...');
|
||||
await api.deleteFeeType(input.feeTypeId);
|
||||
logger.info({ name: 'verband.feeType.delete' }, 'Fee type deleted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Fee Billings (Beitragsabrechnungen)
|
||||
// =====================================================
|
||||
|
||||
export const createFeeBilling = authActionClient
|
||||
.inputSchema(CreateClubFeeBillingSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.billing.create' }, 'Creating fee billing...');
|
||||
const result = await api.createFeeBilling(input);
|
||||
logger.info({ name: 'verband.billing.create' }, 'Fee billing created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateFeeBilling = authActionClient
|
||||
.inputSchema(
|
||||
CreateClubFeeBillingSchema.partial().extend({
|
||||
billingId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
const { billingId, ...updates } = input;
|
||||
logger.info({ name: 'verband.billing.update' }, 'Updating fee billing...');
|
||||
const result = await api.updateFeeBilling(billingId, updates);
|
||||
logger.info({ name: 'verband.billing.update' }, 'Fee billing updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const deleteFeeBilling = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
billingId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.billing.delete' }, 'Deleting fee billing...');
|
||||
await api.deleteFeeBilling(input.billingId);
|
||||
logger.info({ name: 'verband.billing.delete' }, 'Fee billing deleted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
export const markBillingPaid = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
billingId: z.string().uuid(),
|
||||
paidDate: z.string().optional(),
|
||||
paymentMethod: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.billing.markPaid' }, 'Marking billing as paid...');
|
||||
const result = await api.updateFeeBilling(input.billingId, {
|
||||
status: 'bezahlt',
|
||||
paidDate: input.paidDate ?? new Date().toISOString().split('T')[0],
|
||||
paymentMethod: input.paymentMethod as 'bar' | 'lastschrift' | 'ueberweisung' | 'paypal' | undefined,
|
||||
});
|
||||
logger.info({ name: 'verband.billing.markPaid' }, 'Billing marked as paid');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Notes (Notizen / Aufgaben)
|
||||
// =====================================================
|
||||
|
||||
export const createClubNote = authActionClient
|
||||
.inputSchema(CreateClubNoteSchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.note.create' }, 'Creating note...');
|
||||
const result = await api.createNote(input);
|
||||
logger.info({ name: 'verband.note.create' }, 'Note created');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const updateClubNote = authActionClient
|
||||
.inputSchema(
|
||||
CreateClubNoteSchema.partial().extend({
|
||||
noteId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
const { noteId, ...updates } = input;
|
||||
logger.info({ name: 'verband.note.update' }, 'Updating note...');
|
||||
const result = await api.updateNote(noteId, updates);
|
||||
logger.info({ name: 'verband.note.update' }, 'Note updated');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const completeClubNote = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
noteId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.note.complete' }, 'Completing note...');
|
||||
const result = await api.completeNote(input.noteId);
|
||||
logger.info({ name: 'verband.note.complete' }, 'Note completed');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
|
||||
export const deleteClubNote = authActionClient
|
||||
.inputSchema(
|
||||
z.object({
|
||||
noteId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.note.delete' }, 'Deleting note...');
|
||||
await api.deleteNote(input.noteId);
|
||||
logger.info({ name: 'verband.note.delete' }, 'Note deleted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Association History (Verbandshistorie)
|
||||
// =====================================================
|
||||
|
||||
export const upsertAssociationHistory = authActionClient
|
||||
.inputSchema(CreateAssociationHistorySchema)
|
||||
.action(async ({ parsedInput: input }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
logger.info({ name: 'verband.history.upsert' }, 'Upserting association history...');
|
||||
const result = await api.upsertHistory(input);
|
||||
logger.info({ name: 'verband.history.upsert' }, 'Association history upserted');
|
||||
revalidatePath(REVALIDATE_PATH, 'page');
|
||||
return { success: true, data: result };
|
||||
});
|
||||
Reference in New Issue
Block a user