Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -1,15 +1,14 @@
'use client';
import { useState } from 'react';
import { useAction } from 'next-safe-action/hooks';
import { Plus, Pencil, Trash2, Settings } from 'lucide-react';
import { useAction } from 'next-safe-action/hooks';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import {
createRole,
updateRole,
@@ -93,18 +92,27 @@ function SettingsSection({
>
Erstellen
</Button>
<Button size="sm" variant="outline" onClick={() => setShowAdd(false)}>
<Button
size="sm"
variant="outline"
onClick={() => setShowAdd(false)}
>
Abbrechen
</Button>
</div>
)}
{items.length === 0 ? (
<p className="text-sm text-muted-foreground">Keine Einträge vorhanden.</p>
<p className="text-muted-foreground text-sm">
Keine Einträge vorhanden.
</p>
) : (
<div className="space-y-2">
{items.map((item) => (
<div key={String(item.id)} className="flex items-center justify-between rounded-lg border p-3">
<div
key={String(item.id)}
className="flex items-center justify-between rounded-lg border p-3"
>
{editingId === String(item.id) ? (
<div className="flex flex-1 gap-2">
<Input
@@ -122,7 +130,11 @@ function SettingsSection({
>
Speichern
</Button>
<Button size="sm" variant="outline" onClick={() => setEditingId(null)}>
<Button
size="sm"
variant="outline"
onClick={() => setEditingId(null)}
>
Abbrechen
</Button>
</div>
@@ -131,7 +143,9 @@ function SettingsSection({
<div>
<span className="font-medium">{String(item.name)}</span>
{item.description && (
<p className="text-xs text-muted-foreground">{String(item.description)}</p>
<p className="text-muted-foreground text-xs">
{String(item.description)}
</p>
)}
</div>
<div className="flex gap-1">
@@ -150,7 +164,7 @@ function SettingsSection({
size="sm"
onClick={() => onDelete(String(item.id))}
>
<Trash2 className="h-4 w-4 text-destructive" />
<Trash2 className="text-destructive h-4 w-4" />
</Button>
</div>
</>
@@ -171,42 +185,60 @@ export default function SettingsContent({
feeTypes,
}: SettingsContentProps) {
// Roles
const { execute: execCreateRole, isPending: isCreatingRole } = useAction(createRole, {
onSuccess: () => toast.success('Funktion erstellt'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
const { execute: execUpdateRole, isPending: isUpdatingRole } = useAction(updateRole, {
onSuccess: () => toast.success('Funktion aktualisiert'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
const { execute: execCreateRole, isPending: isCreatingRole } = useAction(
createRole,
{
onSuccess: () => toast.success('Funktion erstellt'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
},
);
const { execute: execUpdateRole, isPending: isUpdatingRole } = useAction(
updateRole,
{
onSuccess: () => toast.success('Funktion aktualisiert'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
},
);
const { execute: execDeleteRole } = useAction(deleteRole, {
onSuccess: () => toast.success('Funktion gelöscht'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
// Types
const { execute: execCreateType, isPending: isCreatingType } = useAction(createAssociationType, {
onSuccess: () => toast.success('Vereinstyp erstellt'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
const { execute: execUpdateType, isPending: isUpdatingType } = useAction(updateAssociationType, {
onSuccess: () => toast.success('Vereinstyp aktualisiert'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
const { execute: execCreateType, isPending: isCreatingType } = useAction(
createAssociationType,
{
onSuccess: () => toast.success('Vereinstyp erstellt'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
},
);
const { execute: execUpdateType, isPending: isUpdatingType } = useAction(
updateAssociationType,
{
onSuccess: () => toast.success('Vereinstyp aktualisiert'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
},
);
const { execute: execDeleteType } = useAction(deleteAssociationType, {
onSuccess: () => toast.success('Vereinstyp gelöscht'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
// Fee Types
const { execute: execCreateFeeType, isPending: isCreatingFee } = useAction(createFeeType, {
onSuccess: () => toast.success('Beitragsart erstellt'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
const { execute: execUpdateFeeType, isPending: isUpdatingFee } = useAction(updateFeeType, {
onSuccess: () => toast.success('Beitragsart aktualisiert'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
});
const { execute: execCreateFeeType, isPending: isCreatingFee } = useAction(
createFeeType,
{
onSuccess: () => toast.success('Beitragsart erstellt'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
},
);
const { execute: execUpdateFeeType, isPending: isUpdatingFee } = useAction(
updateFeeType,
{
onSuccess: () => toast.success('Beitragsart aktualisiert'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
},
);
const { execute: execDeleteFeeType } = useAction(deleteFeeType, {
onSuccess: () => toast.success('Beitragsart gelöscht'),
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
@@ -224,7 +256,9 @@ export default function SettingsContent({
<SettingsSection
title="Funktionen (Rollen)"
items={roles}
onAdd={(name, description) => execCreateRole({ accountId, name, description, sortOrder: 0 })}
onAdd={(name, description) =>
execCreateRole({ accountId, name, description, sortOrder: 0 })
}
onUpdate={(id, name) => execUpdateRole({ roleId: id, name })}
onDelete={(id) => execDeleteRole({ roleId: id })}
isAdding={isCreatingRole}
@@ -234,7 +268,9 @@ export default function SettingsContent({
<SettingsSection
title="Vereinstypen"
items={types}
onAdd={(name, description) => execCreateType({ accountId, name, description, sortOrder: 0 })}
onAdd={(name, description) =>
execCreateType({ accountId, name, description, sortOrder: 0 })
}
onUpdate={(id, name) => execUpdateType({ typeId: id, name })}
onDelete={(id) => execDeleteType({ typeId: id })}
isAdding={isCreatingType}
@@ -244,7 +280,9 @@ export default function SettingsContent({
<SettingsSection
title="Beitragsarten"
items={feeTypes}
onAdd={(name, description) => execCreateFeeType({ accountId, name, description, isActive: true })}
onAdd={(name, description) =>
execCreateFeeType({ accountId, name, description, isActive: true })
}
onUpdate={(id, name) => execUpdateFeeType({ feeTypeId: id, name })}
onDelete={(id) => execDeleteFeeType({ feeTypeId: id })}
isAdding={isCreatingFee}

View File

@@ -2,10 +2,10 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { VerbandTabNavigation } from '@kit/verbandsverwaltung/components';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import SettingsContent from './_components/settings-content';
import { AccountNotFound } from '~/components/account-not-found';
interface Props {
params: Promise<{ account: string }>;