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,13 +1,17 @@
'use client';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import { toast } from '@kit/ui/sonner';
import { useAction } from 'next-safe-action/hooks';
import { useForm } from 'react-hook-form';
import { formatDate } from '@kit/shared/dates';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { toast } from '@kit/ui/sonner';
import {
STATUS_LABELS,
@@ -25,16 +29,26 @@ interface MemberDetailViewProps {
accountId: string;
}
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
function DetailRow({
label,
value,
}: {
label: string;
value: React.ReactNode;
}) {
return (
<div className="flex items-start justify-between gap-4 border-b py-2 last:border-b-0">
<span className="text-sm font-medium text-muted-foreground">{label}</span>
<span className="text-sm text-right">{value ?? '—'}</span>
<span className="text-muted-foreground text-sm font-medium">{label}</span>
<span className="text-right text-sm">{value ?? '—'}</span>
</div>
);
}
export function MemberDetailView({ member, account, accountId }: MemberDetailViewProps) {
export function MemberDetailView({
member,
account,
accountId,
}: MemberDetailViewProps) {
const router = useRouter();
const memberId = String(member.id ?? '');
@@ -45,32 +59,42 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
const form = useForm();
const { execute: executeDelete, isPending: isDeleting } = useAction(deleteMember, {
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Mitglied wurde gekündigt');
router.push(`/home/${account}/members-cms`);
}
const { execute: executeDelete, isPending: isDeleting } = useAction(
deleteMember,
{
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Mitglied wurde gekündigt');
router.push(`/home/${account}/members-cms`);
}
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Kündigen');
},
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Kündigen');
},
});
);
const { execute: executeUpdate, isPending: isUpdating } = useAction(updateMember, {
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Mitglied wurde archiviert');
router.refresh();
}
const { execute: executeUpdate, isPending: isUpdating } = useAction(
updateMember,
{
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Mitglied wurde archiviert');
router.refresh();
}
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Archivieren');
},
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Archivieren');
},
});
);
const handleDelete = useCallback(() => {
if (!window.confirm(`Möchten Sie ${fullName} wirklich kündigen? Diese Aktion kann nicht rückgängig gemacht werden.`)) {
if (
!window.confirm(
`Möchten Sie ${fullName} wirklich kündigen? Diese Aktion kann nicht rückgängig gemacht werden.`,
)
) {
return;
}
executeDelete({ memberId, accountId });
@@ -88,7 +112,9 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
}, [executeUpdate, memberId, accountId, fullName]);
const age = computeAge(member.date_of_birth as string | null | undefined);
const membershipYears = computeMembershipYears(member.entry_date as string | null | undefined);
const membershipYears = computeMembershipYears(
member.entry_date as string | null | undefined,
);
const address = formatAddress(member);
const iban = formatIban(member.iban as string | null | undefined);
@@ -103,7 +129,7 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
{STATUS_LABELS[status] ?? status}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
<p className="text-muted-foreground text-sm">
Mitgliedsnr. {String(member.member_number ?? '—')}
</p>
</div>
@@ -147,12 +173,18 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
label="Geburtsdatum"
value={
member.date_of_birth
? `${new Date(String(member.date_of_birth)).toLocaleDateString('de-DE')}${age !== null ? ` (${age} Jahre)` : ''}`
? `${formatDate(member.date_of_birth as string)}${age !== null ? ` (${age} Jahre)` : ''}`
: null
}
/>
<DetailRow label="Geschlecht" value={String(member.gender ?? '—')} />
<DetailRow label="Anrede" value={String(member.salutation ?? '—')} />
<DetailRow
label="Geschlecht"
value={String(member.gender ?? '—')}
/>
<DetailRow
label="Anrede"
value={String(member.salutation ?? '—')}
/>
</CardContent>
</Card>
@@ -187,7 +219,10 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
<CardTitle>Mitgliedschaft</CardTitle>
</CardHeader>
<CardContent>
<DetailRow label="Mitgliedsnr." value={String(member.member_number ?? '—')} />
<DetailRow
label="Mitgliedsnr."
value={String(member.member_number ?? '—')}
/>
<DetailRow
label="Status"
value={
@@ -198,11 +233,7 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
/>
<DetailRow
label="Eintrittsdatum"
value={
member.entry_date
? new Date(String(member.entry_date)).toLocaleDateString('de-DE')
: '—'
}
value={formatDate(member.entry_date as string)}
/>
<DetailRow
label="Mitgliedsjahre"
@@ -210,7 +241,10 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
/>
<DetailRow label="IBAN" value={iban} />
<DetailRow label="BIC" value={String(member.bic ?? '—')} />
<DetailRow label="Kontoinhaber" value={String(member.account_holder ?? '—')} />
<DetailRow
label="Kontoinhaber"
value={String(member.account_holder ?? '—')}
/>
<DetailRow label="Notizen" value={String(member.notes ?? '—')} />
</CardContent>
</Card>