Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -7,8 +7,8 @@ import {
|
||||
ClubNotesList,
|
||||
} from '@kit/verbandsverwaltung/components';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string; clubId: string }>;
|
||||
@@ -41,9 +41,11 @@ export default async function ClubDetailPage({ params }: Props) {
|
||||
{detail.club.short_name && (
|
||||
<p className="text-muted-foreground">{detail.club.short_name}</p>
|
||||
)}
|
||||
<div className="mt-2 flex flex-wrap gap-4 text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground mt-2 flex flex-wrap gap-4 text-sm">
|
||||
{detail.club.city && (
|
||||
<span>{detail.club.zip} {detail.club.city}</span>
|
||||
<span>
|
||||
{detail.club.zip} {detail.club.city}
|
||||
</span>
|
||||
)}
|
||||
{detail.club.member_count != null && (
|
||||
<span>{detail.club.member_count} Mitglieder</span>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
|
||||
import { VerbandTabNavigation, CreateClubForm } from '@kit/verbandsverwaltung/components';
|
||||
import {
|
||||
VerbandTabNavigation,
|
||||
CreateClubForm,
|
||||
} from '@kit/verbandsverwaltung/components';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
|
||||
import { VerbandTabNavigation, ClubsDataTable } from '@kit/verbandsverwaltung/components';
|
||||
import {
|
||||
VerbandTabNavigation,
|
||||
ClubsDataTable,
|
||||
} from '@kit/verbandsverwaltung/components';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
|
||||
import {
|
||||
VerbandTabNavigation,
|
||||
HierarchyTree,
|
||||
} from '@kit/verbandsverwaltung/components';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function HierarchyPage({ params }: PageProps) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
if (!acct) return <AccountNotFound />;
|
||||
|
||||
const api = createVerbandApi(client);
|
||||
|
||||
const [tree, availableAccounts] = await Promise.all([
|
||||
api.getHierarchyTree(acct.id),
|
||||
api.searchAvailableAccounts(acct.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<CmsPageShell
|
||||
account={account}
|
||||
title="Verbandsverwaltung - Hierarchie"
|
||||
description="Verwalten Sie die Organisationsstruktur Ihres Verbands"
|
||||
>
|
||||
<VerbandTabNavigation account={account} activeTab="hierarchy" />
|
||||
<HierarchyTree
|
||||
accountId={acct.id}
|
||||
tree={tree}
|
||||
availableAccounts={availableAccounts}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
|
||||
import { VerbandTabNavigation, VerbandDashboard } from '@kit/verbandsverwaltung/components';
|
||||
import {
|
||||
VerbandTabNavigation,
|
||||
VerbandDashboard,
|
||||
} from '@kit/verbandsverwaltung/components';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import {
|
||||
AreaChart,
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
const PLACEHOLDER_DATA = [
|
||||
{ year: '2020', vereine: 12, mitglieder: 850 },
|
||||
{ year: '2021', vereine: 14, mitglieder: 920 },
|
||||
@@ -89,9 +90,10 @@ export default function StatisticsContent() {
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Die Statistiken werden automatisch aus den Vereinsdaten und der Verbandshistorie berechnet.
|
||||
Pflegen Sie die Mitgliederzahlen in den einzelnen Vereinsdetails, um aktuelle Auswertungen zu erhalten.
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Die Statistiken werden automatisch aus den Vereinsdaten und der
|
||||
Verbandshistorie berechnet. Pflegen Sie die Mitgliederzahlen in den
|
||||
einzelnen Vereinsdetails, um aktuelle Auswertungen zu erhalten.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user