feat: enhance member management features; add quick stats and search capabilities
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Member Management', () => {
|
||||
test('create member, edit, search, filter by status', async ({ page: _page }) => {
|
||||
test('create member, edit, search, filter by status', async ({
|
||||
page: _page,
|
||||
}) => {
|
||||
await page.goto('/auth/sign-in');
|
||||
await page.fill('input[name="email"]', 'test@example.com');
|
||||
await page.fill('input[name="password"]', 'testpassword123');
|
||||
|
||||
@@ -457,10 +457,7 @@ export function FeatureCarousel() {
|
||||
const [active, setActive] = useState(0);
|
||||
const slide = SLIDES[active]!;
|
||||
|
||||
const next = useCallback(
|
||||
() => setActive((i) => (i + 1) % SLIDES.length),
|
||||
[],
|
||||
);
|
||||
const next = useCallback(() => setActive((i) => (i + 1) % SLIDES.length), []);
|
||||
const prev = useCallback(
|
||||
() => setActive((i) => (i - 1 + SLIDES.length) % SLIDES.length),
|
||||
[],
|
||||
|
||||
@@ -8,10 +8,7 @@ import { Check, ExternalLink, X } from 'lucide-react';
|
||||
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
} from '@kit/ui/card';
|
||||
import { Card, CardContent } from '@kit/ui/card';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -265,7 +262,10 @@ function PriceBar({
|
||||
<div className="bg-muted h-6 overflow-hidden rounded-md">
|
||||
{available ? (
|
||||
<div
|
||||
className={cn('h-full rounded-md transition-all duration-500', color)}
|
||||
className={cn(
|
||||
'h-full rounded-md transition-all duration-500',
|
||||
color,
|
||||
)}
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
minWidth: pct > 0 ? 4 : 0,
|
||||
@@ -323,7 +323,10 @@ export function PricingCalculator() {
|
||||
<div className="mx-auto w-full max-w-4xl space-y-0">
|
||||
{/* ── Header ── */}
|
||||
<div className="bg-primary rounded-t-2xl px-8 py-7">
|
||||
<Badge variant="outline" className="border-primary-foreground/20 text-primary-foreground/80 mb-1 text-[10px] uppercase tracking-widest">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary-foreground/20 text-primary-foreground/80 mb-1 text-[10px] tracking-widest uppercase"
|
||||
>
|
||||
Preisvergleich
|
||||
</Badge>
|
||||
<h2 className="font-heading text-primary-foreground text-2xl font-bold tracking-tight">
|
||||
@@ -376,7 +379,7 @@ export function PricingCalculator() {
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Card className="bg-primary/5 border-primary/10 flex flex-1 items-center justify-between p-4">
|
||||
<div>
|
||||
<div className="text-primary text-[10px] font-bold uppercase tracking-wider">
|
||||
<div className="text-primary text-[10px] font-bold tracking-wider uppercase">
|
||||
Ihr MYeasyCMS-Tarif
|
||||
</div>
|
||||
<div className="font-heading text-primary text-xl font-bold">
|
||||
@@ -460,21 +463,20 @@ export function PricingCalculator() {
|
||||
{bestSaving && bestSaving.p !== null && bestSaving.p > tier.price && (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Card className="bg-primary/5 border-primary/10 p-5 text-center">
|
||||
<div className="text-primary text-[10px] font-bold uppercase tracking-wider">
|
||||
<div className="text-primary text-[10px] font-bold tracking-wider uppercase">
|
||||
Ersparnis vs. {bestSaving.name.split(' ')[0]}
|
||||
</div>
|
||||
<div className="font-heading text-primary mt-1 text-3xl font-bold">
|
||||
{fmt((bestSaving.p - tier.price) * 12)} €
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
pro Jahr (
|
||||
{Math.round((1 - tier.price / bestSaving.p) * 100)}%
|
||||
pro Jahr ({Math.round((1 - tier.price / bestSaving.p) * 100)}%
|
||||
günstiger)
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-muted/50 p-5 text-center">
|
||||
<div className="text-muted-foreground text-[10px] font-bold uppercase tracking-wider">
|
||||
<div className="text-muted-foreground text-[10px] font-bold tracking-wider uppercase">
|
||||
Preis pro Mitglied
|
||||
</div>
|
||||
<div className="font-heading text-primary mt-1 text-3xl font-bold">
|
||||
@@ -521,19 +523,19 @@ export function PricingCalculator() {
|
||||
{USP_FEATURES.map((f, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell className="font-medium">{f.label}</TableCell>
|
||||
{(
|
||||
['mcms', 'sewobe', 'easy', 'club', 'wiso'] as const
|
||||
).map((col) => (
|
||||
<TableCell
|
||||
key={col}
|
||||
className={cn(
|
||||
'text-center',
|
||||
col === 'mcms' && 'bg-primary/5',
|
||||
)}
|
||||
>
|
||||
<FeatureCell value={f[col]} />
|
||||
</TableCell>
|
||||
))}
|
||||
{(['mcms', 'sewobe', 'easy', 'club', 'wiso'] as const).map(
|
||||
(col) => (
|
||||
<TableCell
|
||||
key={col}
|
||||
className={cn(
|
||||
'text-center',
|
||||
col === 'mcms' && 'bg-primary/5',
|
||||
)}
|
||||
>
|
||||
<FeatureCell value={f[col]} />
|
||||
</TableCell>
|
||||
),
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -23,7 +23,7 @@ export default async function EditMemberPage({ params }: Props) {
|
||||
if (!acct) return <AccountNotFound />;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const member = await api.getMember(memberId);
|
||||
const member = await api.getMember(acct.id, memberId);
|
||||
if (!member) return <div>{t('detail.notFound')}</div>;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { MemberDetailView } from '@kit/member-management/components';
|
||||
import { MemberDetailTabs } from '@kit/member-management/components';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string; memberId: string }>;
|
||||
@@ -20,10 +19,9 @@ export default async function MemberDetailPage({ params }: Props) {
|
||||
if (!acct) return <AccountNotFound />;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const member = await api.getMember(memberId);
|
||||
const member = await api.getMember(acct.id, memberId);
|
||||
if (!member) return <AccountNotFound />;
|
||||
|
||||
// Fetch sub-entities in parallel
|
||||
const [roles, honors, mandates] = await Promise.all([
|
||||
api.listMemberRoles(memberId),
|
||||
api.listMemberHonors(memberId),
|
||||
@@ -31,18 +29,13 @@ export default async function MemberDetailPage({ params }: Props) {
|
||||
]);
|
||||
|
||||
return (
|
||||
<CmsPageShell
|
||||
<MemberDetailTabs
|
||||
member={member}
|
||||
account={account}
|
||||
title={`${String(member.first_name)} ${String(member.last_name)}`}
|
||||
>
|
||||
<MemberDetailView
|
||||
member={member}
|
||||
account={account}
|
||||
accountId={acct.id}
|
||||
roles={roles}
|
||||
honors={honors}
|
||||
mandates={mandates}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
accountId={acct.id}
|
||||
roles={roles}
|
||||
honors={honors}
|
||||
mandates={mandates}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import {
|
||||
FileDown,
|
||||
FileUp,
|
||||
IdCard,
|
||||
KeyRound,
|
||||
LayoutList,
|
||||
Settings,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
MemberStatsBar,
|
||||
MemberCommandPalette,
|
||||
} from '@kit/member-management/components';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
interface MembersCmsLayoutClientProps {
|
||||
header: ReactNode;
|
||||
children: ReactNode;
|
||||
account: string;
|
||||
accountId: string;
|
||||
stats: {
|
||||
total: number;
|
||||
active: number;
|
||||
pending: number;
|
||||
newThisYear: number;
|
||||
pendingApplications: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function MembersCmsLayoutClient({
|
||||
header,
|
||||
children,
|
||||
account,
|
||||
accountId,
|
||||
stats,
|
||||
}: MembersCmsLayoutClientProps) {
|
||||
const pathname = usePathname();
|
||||
const basePath = `/home/${account}/members-cms`;
|
||||
|
||||
const isOnMembersTab =
|
||||
pathname.endsWith('/members-cms') ||
|
||||
pathname.includes('/members-cms/new') ||
|
||||
/\/members-cms\/[^/]+$/.test(pathname);
|
||||
const isOnApplicationsTab = pathname.includes('/applications');
|
||||
const isOnSubPage =
|
||||
pathname.includes('/import') ||
|
||||
pathname.includes('/edit') ||
|
||||
(/\/members-cms\/[^/]+$/.test(pathname) &&
|
||||
!pathname.endsWith('/members-cms'));
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
|
||||
<PageBody>
|
||||
<div className="space-y-4">
|
||||
{/* Stats bar — only on main views */}
|
||||
{!isOnSubPage && <MemberStatsBar stats={stats} />}
|
||||
|
||||
{/* Tab navigation + settings */}
|
||||
{!isOnSubPage && (
|
||||
<div className="flex items-center justify-between border-b">
|
||||
<nav className="-mb-px flex gap-4">
|
||||
<TabLink
|
||||
href={basePath}
|
||||
active={isOnMembersTab && !isOnApplicationsTab}
|
||||
>
|
||||
<Users className="size-4" />
|
||||
Mitglieder
|
||||
</TabLink>
|
||||
<TabLink
|
||||
href={`${basePath}/applications`}
|
||||
active={isOnApplicationsTab}
|
||||
>
|
||||
<FileUp className="size-4" />
|
||||
Aufnahmeanträge
|
||||
{stats.pendingApplications > 0 && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="ml-1 h-5 min-w-5 px-1 text-xs"
|
||||
>
|
||||
{stats.pendingApplications}
|
||||
</Badge>
|
||||
)}
|
||||
</TabLink>
|
||||
</nav>
|
||||
|
||||
<SettingsMenu basePath={basePath} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<MemberCommandPalette account={account} accountId={accountId} />
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TabLink({
|
||||
href,
|
||||
active,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
active: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 border-b-2 px-1 pb-2 text-sm font-medium transition-colors',
|
||||
active
|
||||
? 'border-primary text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground border-transparent',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsMenu({ basePath }: { basePath: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
const navigate = (path: string) => () => router.push(path);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="hover:bg-muted inline-flex size-7 items-center justify-center rounded-lg"
|
||||
data-test="members-settings-menu"
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuItem onClick={navigate(`${basePath}/dues`)}>
|
||||
<LayoutList className="mr-2 size-4" />
|
||||
Beitragskategorien
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={navigate(`${basePath}/departments`)}>
|
||||
<Users className="mr-2 size-4" />
|
||||
Abteilungen
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={navigate(`${basePath}/cards`)}>
|
||||
<IdCard className="mr-2 size-4" />
|
||||
Mitgliedsausweise
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={navigate(`${basePath}/invitations`)}>
|
||||
<KeyRound className="mr-2 size-4" />
|
||||
Portal-Einladungen
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={navigate(`${basePath}/import`)}>
|
||||
<FileDown className="mr-2 size-4" />
|
||||
Import
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { ApplicationWorkflow } from '@kit/member-management/components';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -14,7 +11,7 @@ interface Props {
|
||||
export default async function ApplicationsPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const t = await getTranslations('members');
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
@@ -26,16 +23,10 @@ export default async function ApplicationsPage({ params }: Props) {
|
||||
const applications = await api.listApplications(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell
|
||||
<ApplicationWorkflow
|
||||
applications={applications}
|
||||
accountId={acct.id}
|
||||
account={account}
|
||||
title={t('nav.applications')}
|
||||
description={t('applications.subtitle')}
|
||||
>
|
||||
<ApplicationWorkflow
|
||||
applications={applications}
|
||||
accountId={acct.id}
|
||||
account={account}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
47
apps/web/app/[locale]/home/[account]/members-cms/layout.tsx
Normal file
47
apps/web/app/[locale]/home/[account]/members-cms/layout.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { TeamAccountLayoutPageHeader } from '~/home/[account]/_components/team-account-layout-page-header';
|
||||
|
||||
import { MembersCmsLayoutClient } from './_components/members-cms-layout-client';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function MembersCmsLayout({ children, params }: Props) {
|
||||
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 = createMemberManagementApi(client);
|
||||
const stats = await api.getMemberQuickStats(acct.id);
|
||||
|
||||
return (
|
||||
<MembersCmsLayoutClient
|
||||
header={
|
||||
<TeamAccountLayoutPageHeader
|
||||
account={account}
|
||||
title="Mitglieder"
|
||||
description={`${stats.total} Mitglieder verwalten`}
|
||||
/>
|
||||
}
|
||||
account={account}
|
||||
accountId={acct.id}
|
||||
stats={stats}
|
||||
>
|
||||
{children}
|
||||
</MembersCmsLayoutClient>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { CreateMemberForm } from '@kit/member-management/components';
|
||||
import { MemberCreateWizard } from '@kit/member-management/components';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -13,7 +10,6 @@ interface Props {
|
||||
|
||||
export default async function NewMemberPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const t = await getTranslations('members');
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
@@ -26,22 +22,16 @@ export default async function NewMemberPage({ params }: Props) {
|
||||
const duesCategories = await api.listDuesCategories(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell
|
||||
<MemberCreateWizard
|
||||
accountId={acct.id}
|
||||
account={account}
|
||||
title={t('form.newMemberTitle')}
|
||||
description={t('form.newMemberDescription')}
|
||||
>
|
||||
<CreateMemberForm
|
||||
accountId={acct.id}
|
||||
account={account}
|
||||
duesCategories={(duesCategories ?? []).map(
|
||||
(c: Record<string, unknown>) => ({
|
||||
id: String(c.id),
|
||||
name: String(c.name),
|
||||
amount: Number(c.amount ?? 0),
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
duesCategories={(duesCategories ?? []).map(
|
||||
(c: Record<string, unknown>) => ({
|
||||
id: String(c.id),
|
||||
name: String(c.name),
|
||||
amount: Number(c.amount ?? 0),
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { MembersDataTable } from '@kit/member-management/components';
|
||||
import { MembersListView } from '@kit/member-management/components';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
@@ -18,7 +15,7 @@ export default async function MembersPage({ params, searchParams }: Props) {
|
||||
const { account } = await params;
|
||||
const search = await searchParams;
|
||||
const client = getSupabaseServerClient();
|
||||
const t = await getTranslations('members');
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
@@ -28,34 +25,50 @@ export default async function MembersPage({ params, searchParams }: Props) {
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const page = Number(search.page) || 1;
|
||||
const result = await api.listMembers(acct.id, {
|
||||
|
||||
// Parse multi-status filter
|
||||
const statusParam = search.status;
|
||||
const statusFilter = statusParam
|
||||
? Array.isArray(statusParam)
|
||||
? statusParam
|
||||
: statusParam.split(',')
|
||||
: undefined;
|
||||
|
||||
const result = await api.searchMembers({
|
||||
accountId: acct.id,
|
||||
search: search.q as string,
|
||||
status: search.status as string,
|
||||
status: statusFilter as any,
|
||||
duesCategoryId: search.duesCategoryId as string,
|
||||
sortBy: (search.sortBy as string) ?? 'last_name',
|
||||
sortDirection: (search.sortDirection as 'asc' | 'desc') ?? 'asc',
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
const duesCategories = await api.listDuesCategories(acct.id);
|
||||
|
||||
const [duesCategories, departments] = await Promise.all([
|
||||
api.listDuesCategories(acct.id),
|
||||
api.listDepartmentsWithCounts(acct.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<CmsPageShell
|
||||
<MembersListView
|
||||
data={result.data}
|
||||
total={result.total}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
account={account}
|
||||
title={t('nav.members')}
|
||||
description={`${result.total} ${t('nav.members')}`}
|
||||
>
|
||||
<MembersDataTable
|
||||
data={result.data}
|
||||
total={result.total}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
account={account}
|
||||
accountId={acct.id}
|
||||
duesCategories={(duesCategories ?? []).map(
|
||||
(c: Record<string, unknown>) => ({
|
||||
id: String(c.id),
|
||||
name: String(c.name),
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
accountId={acct.id}
|
||||
duesCategories={(duesCategories ?? []).map(
|
||||
(c: Record<string, unknown>) => ({
|
||||
id: String(c.id),
|
||||
name: String(c.name),
|
||||
}),
|
||||
)}
|
||||
departments={(departments ?? []).map((d) => ({
|
||||
id: String(d.id),
|
||||
name: String(d.name),
|
||||
memberCount: d.memberCount,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { FileText, Plus } from 'lucide-react';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@ import {
|
||||
CreditCard,
|
||||
// People (Members + Access)
|
||||
UserCheck,
|
||||
UserPlus,
|
||||
IdCard,
|
||||
ClipboardList,
|
||||
// Courses
|
||||
GraduationCap,
|
||||
CalendarDays,
|
||||
@@ -69,7 +66,10 @@ import pathsConfig from '~/config/paths.config';
|
||||
|
||||
const iconClasses = 'w-4';
|
||||
|
||||
const getRoutes = (account: string, accountFeatures?: Record<string, boolean>) => {
|
||||
const getRoutes = (
|
||||
account: string,
|
||||
accountFeatures?: Record<string, boolean>,
|
||||
) => {
|
||||
const routes: Array<
|
||||
| {
|
||||
label: string;
|
||||
@@ -110,46 +110,11 @@ const getRoutes = (account: string, accountFeatures?: Record<string, boolean>) =
|
||||
}> = [];
|
||||
|
||||
if (featureFlagsConfig.enableMemberManagement) {
|
||||
peopleChildren.push(
|
||||
{
|
||||
label: 'common.routes.clubMembers',
|
||||
path: createPath(pathsConfig.app.accountCmsMembers, account),
|
||||
Icon: <UserCheck className={iconClasses} />,
|
||||
},
|
||||
{
|
||||
label: 'common.routes.memberApplications',
|
||||
path: createPath(
|
||||
pathsConfig.app.accountCmsMembers + '/applications',
|
||||
account,
|
||||
),
|
||||
Icon: <UserPlus className={iconClasses} />,
|
||||
},
|
||||
// NOTE: memberPortal page does not exist yet — nav entry commented out until built
|
||||
// {
|
||||
// label: 'common.routes.memberPortal',
|
||||
// path: createPath(
|
||||
// pathsConfig.app.accountCmsMembers + '/portal',
|
||||
// account,
|
||||
// ),
|
||||
// Icon: <KeyRound className={iconClasses} />,
|
||||
// },
|
||||
{
|
||||
label: 'common.routes.memberCards',
|
||||
path: createPath(
|
||||
pathsConfig.app.accountCmsMembers + '/cards',
|
||||
account,
|
||||
),
|
||||
Icon: <IdCard className={iconClasses} />,
|
||||
},
|
||||
{
|
||||
label: 'common.routes.memberDues',
|
||||
path: createPath(
|
||||
pathsConfig.app.accountCmsMembers + '/dues',
|
||||
account,
|
||||
),
|
||||
Icon: <ClipboardList className={iconClasses} />,
|
||||
},
|
||||
);
|
||||
peopleChildren.push({
|
||||
label: 'common.routes.clubMembers',
|
||||
path: createPath(pathsConfig.app.accountCmsMembers, account),
|
||||
Icon: <UserCheck className={iconClasses} />,
|
||||
});
|
||||
}
|
||||
|
||||
// Admin users who can log in — always visible
|
||||
@@ -417,7 +382,10 @@ const getRoutes = (account: string, accountFeatures?: Record<string, boolean>) =
|
||||
}
|
||||
|
||||
// ── Fisheries ──
|
||||
if (featureFlagsConfig.enableFischerei && (accountFeatures?.fischerei !== false)) {
|
||||
if (
|
||||
featureFlagsConfig.enableFischerei &&
|
||||
accountFeatures?.fischerei !== false
|
||||
) {
|
||||
routes.push({
|
||||
label: 'common.routes.fisheriesManagement',
|
||||
collapsible: true,
|
||||
@@ -473,7 +441,10 @@ const getRoutes = (account: string, accountFeatures?: Record<string, boolean>) =
|
||||
}
|
||||
|
||||
// ── Meeting Protocols ──
|
||||
if (featureFlagsConfig.enableMeetingProtocols && (accountFeatures?.meetings !== false)) {
|
||||
if (
|
||||
featureFlagsConfig.enableMeetingProtocols &&
|
||||
accountFeatures?.meetings !== false
|
||||
) {
|
||||
routes.push({
|
||||
label: 'common.routes.meetingProtocols',
|
||||
collapsible: true,
|
||||
@@ -502,7 +473,10 @@ const getRoutes = (account: string, accountFeatures?: Record<string, boolean>) =
|
||||
}
|
||||
|
||||
// ── Association Management (Verband) ──
|
||||
if (featureFlagsConfig.enableVerbandsverwaltung && (accountFeatures?.verband !== false)) {
|
||||
if (
|
||||
featureFlagsConfig.enableVerbandsverwaltung &&
|
||||
accountFeatures?.verband !== false
|
||||
) {
|
||||
routes.push({
|
||||
label: 'common.routes.associationManagement',
|
||||
collapsible: true,
|
||||
|
||||
@@ -6404,6 +6404,22 @@ export type Database = {
|
||||
total_upcoming_events: number
|
||||
}[]
|
||||
}
|
||||
get_member_quick_stats: {
|
||||
Args: { p_account_id: string }
|
||||
Returns: {
|
||||
active: number
|
||||
inactive: number
|
||||
new_this_year: number
|
||||
pending: number
|
||||
pending_applications: number
|
||||
resigned: number
|
||||
total: number
|
||||
}[]
|
||||
}
|
||||
get_next_member_number: {
|
||||
Args: { p_account_id: string }
|
||||
Returns: string
|
||||
}
|
||||
get_nonce_status: { Args: { p_id: string }; Returns: Json }
|
||||
get_upper_system_role: { Args: never; Returns: string }
|
||||
get_user_visible_accounts: { Args: never; Returns: string[] }
|
||||
|
||||
@@ -41,6 +41,7 @@ const INTERNAL_PACKAGES = [
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
/** Enables hot reloading for local packages without a build step */
|
||||
transpilePackages: INTERNAL_PACKAGES,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
-- Migration: Enhanced member search and quick stats
|
||||
-- Adds: full-text search index, quick stats RPC, next member number function
|
||||
|
||||
-- Full-text search index (German) for faster member search
|
||||
CREATE INDEX IF NOT EXISTS ix_members_fulltext ON public.members
|
||||
USING gin(
|
||||
to_tsvector(
|
||||
'german',
|
||||
coalesce(first_name, '') || ' ' ||
|
||||
coalesce(last_name, '') || ' ' ||
|
||||
coalesce(email, '') || ' ' ||
|
||||
coalesce(member_number, '') || ' ' ||
|
||||
coalesce(city, '')
|
||||
)
|
||||
);
|
||||
|
||||
-- Trigram index on names for fuzzy / ILIKE search
|
||||
CREATE INDEX IF NOT EXISTS ix_members_name_trgm
|
||||
ON public.members
|
||||
USING gin ((lower(first_name || ' ' || last_name)) gin_trgm_ops);
|
||||
|
||||
-- Quick stats RPC — returns a single row with KPI counts
|
||||
-- Includes has_role_on_account guard to prevent cross-tenant data leaks
|
||||
CREATE OR REPLACE FUNCTION public.get_member_quick_stats(p_account_id uuid)
|
||||
RETURNS TABLE(
|
||||
total bigint,
|
||||
active bigint,
|
||||
inactive bigint,
|
||||
pending bigint,
|
||||
resigned bigint,
|
||||
new_this_year bigint,
|
||||
pending_applications bigint
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Verify caller has access to this account
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied to account %', p_account_id;
|
||||
END IF;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
count(*)::bigint AS total,
|
||||
count(*) FILTER (WHERE m.status = 'active')::bigint AS active,
|
||||
count(*) FILTER (WHERE m.status = 'inactive')::bigint AS inactive,
|
||||
count(*) FILTER (WHERE m.status = 'pending')::bigint AS pending,
|
||||
count(*) FILTER (WHERE m.status = 'resigned')::bigint AS resigned,
|
||||
count(*) FILTER (WHERE m.status = 'active'
|
||||
AND m.entry_date >= date_trunc('year', current_date)::date)::bigint AS new_this_year,
|
||||
(
|
||||
SELECT count(*)
|
||||
FROM public.membership_applications a
|
||||
WHERE a.account_id = p_account_id
|
||||
AND a.status = 'submitted'
|
||||
)::bigint AS pending_applications
|
||||
FROM public.members m
|
||||
WHERE m.account_id = p_account_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_member_quick_stats(uuid) TO authenticated;
|
||||
|
||||
-- Next member number: returns max(member_number) + 1 as text
|
||||
-- Includes has_role_on_account guard
|
||||
CREATE OR REPLACE FUNCTION public.get_next_member_number(p_account_id uuid)
|
||||
RETURNS text
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
v_result text;
|
||||
BEGIN
|
||||
-- Verify caller has access to this account
|
||||
IF NOT public.has_role_on_account(p_account_id) THEN
|
||||
RAISE EXCEPTION 'Access denied to account %', p_account_id;
|
||||
END IF;
|
||||
|
||||
SELECT LPAD(
|
||||
(COALESCE(
|
||||
MAX(
|
||||
CASE
|
||||
WHEN member_number ~ '^\d+$' THEN member_number::integer
|
||||
ELSE 0
|
||||
END
|
||||
),
|
||||
0
|
||||
) + 1)::text,
|
||||
4,
|
||||
'0'
|
||||
) INTO v_result
|
||||
FROM public.members
|
||||
WHERE account_id = p_account_id;
|
||||
|
||||
RETURN v_result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.get_next_member_number(uuid) TO authenticated;
|
||||
@@ -285,3 +285,146 @@ SELECT pg_catalog.setval('"public"."role_permissions_id_seq"', 7, true);
|
||||
--
|
||||
|
||||
SELECT pg_catalog.setval('"supabase_functions"."hooks_id_seq"', 19, true);
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- Member Management Seed Data
|
||||
-- 30 realistic German/Austrian club members for demo/development
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_account_id uuid := '5deaa894-2094-4da3-b4fd-1fada0809d1c';
|
||||
v_user_id uuid := '31a03e74-1639-45b6-bfa7-77447f1a4762';
|
||||
v_cat_regular uuid;
|
||||
v_cat_youth uuid;
|
||||
v_cat_senior uuid;
|
||||
v_dept_vorstand uuid;
|
||||
v_dept_jugend uuid;
|
||||
v_dept_sport uuid;
|
||||
v_m1 uuid; v_m2 uuid; v_m3 uuid; v_m4 uuid; v_m5 uuid;
|
||||
v_m6 uuid; v_m7 uuid; v_m8 uuid; v_m9 uuid; v_m10 uuid;
|
||||
BEGIN
|
||||
|
||||
-- Dues Categories
|
||||
INSERT INTO public.dues_categories (id, account_id, name, description, amount, interval, is_default, sort_order)
|
||||
VALUES (gen_random_uuid(), v_account_id, 'Erwachsene', 'Regulärer Mitgliedsbeitrag', 120.00, 'yearly', true, 1)
|
||||
RETURNING id INTO v_cat_regular;
|
||||
|
||||
INSERT INTO public.dues_categories (id, account_id, name, description, amount, interval, is_youth, sort_order)
|
||||
VALUES (gen_random_uuid(), v_account_id, 'Jugend (bis 18)', 'Ermäßigter Jugendbeitrag', 48.00, 'yearly', true, 2)
|
||||
RETURNING id INTO v_cat_youth;
|
||||
|
||||
INSERT INTO public.dues_categories (id, account_id, name, description, amount, interval, sort_order)
|
||||
VALUES (gen_random_uuid(), v_account_id, 'Senioren (ab 65)', 'Ermäßigter Seniorenbeitrag', 72.00, 'yearly', 3)
|
||||
RETURNING id INTO v_cat_senior;
|
||||
|
||||
-- Departments
|
||||
INSERT INTO public.member_departments (id, account_id, name, description, sort_order)
|
||||
VALUES (gen_random_uuid(), v_account_id, 'Vorstand', 'Vereinsvorstand und Leitung', 1)
|
||||
RETURNING id INTO v_dept_vorstand;
|
||||
|
||||
INSERT INTO public.member_departments (id, account_id, name, description, sort_order)
|
||||
VALUES (gen_random_uuid(), v_account_id, 'Jugendabteilung', 'Kinder- und Jugendarbeit', 2)
|
||||
RETURNING id INTO v_dept_jugend;
|
||||
|
||||
INSERT INTO public.member_departments (id, account_id, name, description, sort_order)
|
||||
VALUES (gen_random_uuid(), v_account_id, 'Sportabteilung', 'Training und Wettkampf', 3)
|
||||
RETURNING id INTO v_dept_sport;
|
||||
|
||||
-- Members 1-10 (with variables for relationships)
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, phone, mobile, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, iban, bic, account_holder, is_founding_member, gdpr_consent, gdpr_newsletter, gdpr_internet, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0001', 'Johann', 'Maier', '1968-03-15', 'male', 'Herr', 'johann.maier@example.at', '+43 512 123456', '+43 664 1234567', 'Hauptstraße', '12', '6020', 'Innsbruck', 'AT', 'active', '2005-01-15', v_cat_regular, 'AT483200000012345864', 'RLNWATWW', 'Johann Maier', true, true, true, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m1;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, phone, mobile, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, iban, bic, account_holder, gdpr_consent, gdpr_newsletter, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0002', 'Maria', 'Huber', '1975-07-22', 'female', 'Frau', 'maria.huber@example.at', '+43 512 234567', '+43 660 2345678', 'Bahnhofstraße', '5a', '6020', 'Innsbruck', 'AT', 'active', '2008-03-01', v_cat_regular, 'AT611904300234573201', 'BKAUATWW', 'Maria Huber', true, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m2;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, phone, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, gdpr_consent, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0003', 'Thomas', 'Berger', '1982-11-08', 'male', 'Herr', 'thomas.berger@example.at', '+43 512 345678', 'Museumstraße', '3', '6020', 'Innsbruck', 'AT', 'active', '2010-06-15', v_cat_regular, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m3;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, mobile, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, gdpr_consent, gdpr_newsletter, gdpr_internet, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0004', 'Anna', 'Steiner', '1990-04-12', 'female', 'Frau', 'anna.steiner@example.at', '+43 676 3456789', 'Leopoldstraße', '18', '6020', 'Innsbruck', 'AT', 'active', '2012-09-01', v_cat_regular, true, true, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m4;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, title, email, phone, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, is_honorary, is_founding_member, gdpr_consent, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0005', 'Franz', 'Gruber', '1945-09-03', 'male', 'Herr', 'Dr.', 'franz.gruber@example.at', '+43 512 456789', 'Rennweg', '7', '6020', 'Innsbruck', 'AT', 'active', '1998-01-01', v_cat_senior, true, true, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m5;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, email, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, is_youth, guardian_name, guardian_phone, guardian_email, gdpr_consent, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0006', 'Lukas', 'Hofer', '2010-02-28', 'male', 'lukas.hofer@example.at', 'Schillerstraße', '22', '6020', 'Innsbruck', 'AT', 'active', '2022-03-01', v_cat_youth, true, 'Stefan Hofer', '+43 664 5678901', 'stefan.hofer@example.at', true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m6;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, phone, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, gdpr_consent, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0007', 'Katharina', 'Wimmer', '1988-12-05', 'female', 'Frau', 'k.wimmer@example.at', '+43 512 567890', 'Maria-Theresien-Straße', '15', '6020', 'Innsbruck', 'AT', 'inactive', '2015-01-01', v_cat_regular, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m7;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, street, house_number, postal_code, city, country, status, entry_date, exit_date, exit_reason, dues_category_id, gdpr_consent, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0008', 'Peter', 'Moser', '1970-06-18', 'male', 'Herr', 'peter.moser@example.at', 'Anichstraße', '29', '6020', 'Innsbruck', 'AT', 'resigned', '2010-05-01', '2025-12-31', 'Umzug', v_cat_regular, false, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m8;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, mobile, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, gdpr_consent, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0009', 'Sophie', 'Eder', '1995-08-30', 'female', 'Frau', 'sophie.eder@example.at', '+43 680 6789012', 'Universitätsstraße', '8', '6020', 'Innsbruck', 'AT', 'pending', '2026-03-15', v_cat_regular, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m9;
|
||||
|
||||
INSERT INTO public.members (id, account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, phone, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, is_retiree, gdpr_consent, gdpr_print, gdpr_birthday_info, created_by, updated_by)
|
||||
VALUES (gen_random_uuid(), v_account_id, '0010', 'Helmut', 'Bauer', '1952-01-14', 'male', 'Herr', 'helmut.bauer@example.at', '+43 512 678901', 'Sillgasse', '14', '6020', 'Innsbruck', 'AT', 'active', '2001-07-01', v_cat_senior, true, true, true, true, v_user_id, v_user_id)
|
||||
RETURNING id INTO v_m10;
|
||||
|
||||
-- Members 11-30 (bulk insert)
|
||||
INSERT INTO public.members (account_id, member_number, first_name, last_name, date_of_birth, gender, salutation, email, mobile, street, house_number, postal_code, city, country, status, entry_date, dues_category_id, gdpr_consent, created_by, updated_by) VALUES
|
||||
(v_account_id, '0011', 'Christina', 'Pichler', '1993-05-17', 'female', 'Frau', 'christina.pichler@example.at', '+43 664 7890123', 'Innrain', '52', '6020', 'Innsbruck', 'AT', 'active', '2019-01-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0012', 'Michael', 'Ebner', '1985-09-23', 'male', 'Herr', 'michael.ebner@example.at', '+43 660 8901234', 'Höttinger Au', '3', '6020', 'Innsbruck', 'AT', 'active', '2017-04-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0013', 'Eva', 'Schwarz', '1978-02-09', 'female', 'Frau', 'eva.schwarz@example.at', '+43 676 9012345', 'Fallmerayerstraße', '6', '6020', 'Innsbruck', 'AT', 'active', '2014-09-15', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0014', 'Stefan', 'Wallner', '1991-11-30', 'male', 'Herr', 'stefan.wallner@example.at', '+43 664 0123456', 'Reichenauer Straße', '44', '6020', 'Innsbruck', 'AT', 'active', '2020-02-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0015', 'Martina', 'Lechner', '1987-04-25', 'female', 'Frau', 'martina.lechner@example.at', '+43 680 1234567', 'Olympiastraße', '10', '6020', 'Innsbruck', 'AT', 'active', '2016-06-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0016', 'Andreas', 'Koller', '1969-08-11', 'male', 'Herr', 'andreas.koller@example.at', '+43 664 2345670', 'Pradler Straße', '72', '6020', 'Innsbruck', 'AT', 'active', '2007-01-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0017', 'Laura', 'Reiter', '2008-07-19', 'female', NULL, 'laura.reiter@example.at', '+43 660 3456701', 'Gabelsbergerstraße', '4', '6020', 'Innsbruck', 'AT', 'active', '2023-01-01', v_cat_youth, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0018', 'Markus', 'Fuchs', '1980-10-02', 'male', 'Herr', 'markus.fuchs@example.at', '+43 676 4567012', 'Egger-Lienz-Straße', '28', '6020', 'Innsbruck', 'AT', 'active', '2013-03-15', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0019', 'Lisa', 'Müller', '1996-01-07', 'female', 'Frau', 'lisa.mueller@example.at', '+43 664 5670123', 'Amraser Straße', '16', '6020', 'Innsbruck', 'AT', 'active', '2021-09-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0020', 'Georg', 'Wagner', '1973-06-14', 'male', 'Herr', 'georg.wagner@example.at', '+43 680 6701234', 'Kaiserjägerstraße', '1', '6020', 'Innsbruck', 'AT', 'active', '2009-11-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0021', 'Claudia', 'Fischer', '1984-12-20', 'female', 'Frau', 'claudia.fischer@example.at', '+43 664 7012345', 'Technikerstraße', '9', '6020', 'Innsbruck', 'AT', 'active', '2018-05-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0022', 'Daniel', 'Wolf', '1998-03-28', 'male', 'Herr', 'daniel.wolf@example.at', '+43 660 8012346', 'Schöpfstraße', '31', '6020', 'Innsbruck', 'AT', 'active', '2022-01-15', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0023', 'Sandra', 'Brunner', '1976-09-06', 'female', 'Frau', NULL, '+43 512 901234', 'Defreggerstraße', '12', '6020', 'Innsbruck', 'AT', 'active', '2011-04-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0024', 'Robert', 'Lang', '1960-11-11', 'male', 'Herr', 'robert.lang@example.at', '+43 512 012345', 'Speckbacherstraße', '21', '6020', 'Innsbruck', 'AT', 'active', '2003-01-01', v_cat_senior, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0025', 'Nina', 'Winkler', '2009-05-03', 'female', NULL, 'nina.winkler@example.at', '+43 664 1230456', 'Müllerstraße', '7', '6020', 'Innsbruck', 'AT', 'active', '2023-09-01', v_cat_youth, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0026', 'Wolfgang', 'Schmid', '1955-04-22', 'male', 'Herr', 'wolfgang.schmid@example.at', '+43 512 2340567', 'Haller Straße', '55', '6020', 'Innsbruck', 'AT', 'inactive', '2000-06-01', v_cat_senior, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0027', 'Sabrina', 'Gruber', '1994-07-15', 'female', 'Frau', 'sabrina.gruber@example.at', '+43 676 3450678', 'Grabenweg', '33', '6020', 'Innsbruck', 'AT', 'active', '2020-11-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0028', 'Patrick', 'Stockinger', '1989-10-09', 'male', 'Herr', 'patrick.stockinger@example.at', '+43 660 4560789', 'Adamgasse', '19', '6020', 'Innsbruck', 'AT', 'pending', '2026-03-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0029', 'Verena', 'Neuner', '1981-01-18', 'female', 'Frau', 'verena.neuner@example.at', '+43 664 5670890', 'Amthorstraße', '2', '6020', 'Innsbruck', 'AT', 'active', '2015-08-01', v_cat_regular, true, v_user_id, v_user_id),
|
||||
(v_account_id, '0030', 'Florian', 'Kofler', '2011-12-25', 'male', NULL, NULL, '+43 664 6780901', 'Hunoldstraße', '11', '6020', 'Innsbruck', 'AT', 'active', '2024-01-01', v_cat_youth, true, v_user_id, v_user_id);
|
||||
|
||||
-- Department Assignments
|
||||
INSERT INTO public.member_department_assignments (member_id, department_id) VALUES
|
||||
(v_m1, v_dept_vorstand), (v_m2, v_dept_vorstand), (v_m3, v_dept_vorstand),
|
||||
(v_m4, v_dept_jugend), (v_m6, v_dept_jugend),
|
||||
(v_m4, v_dept_sport), (v_m10, v_dept_sport);
|
||||
|
||||
-- Roles
|
||||
INSERT INTO public.member_roles (account_id, member_id, role_name, from_date, until_date, is_active) VALUES
|
||||
(v_account_id, v_m1, '1. Vorsitzender', '2015-01-01', NULL, true),
|
||||
(v_account_id, v_m2, 'Kassierin', '2018-01-01', NULL, true),
|
||||
(v_account_id, v_m3, 'Schriftführer', '2018-01-01', NULL, true),
|
||||
(v_account_id, v_m4, 'Jugendleiterin', '2020-01-01', NULL, true),
|
||||
(v_account_id, v_m1, '2. Vorsitzender', '2008-01-01', '2014-12-31', false),
|
||||
(v_account_id, v_m5, '1. Vorsitzender', '1998-01-01', '2014-12-31', false);
|
||||
|
||||
-- Honors
|
||||
INSERT INTO public.member_honors (account_id, member_id, honor_name, honor_date, description) VALUES
|
||||
(v_account_id, v_m5, 'Ehrenmitglied', '2015-01-01', 'Für 17 Jahre als Vorsitzender'),
|
||||
(v_account_id, v_m1, '20 Jahre Mitgliedschaft', '2025-01-15', 'Treueehrung'),
|
||||
(v_account_id, v_m5, 'Goldene Ehrennadel', '2010-06-15', 'Verdienstauszeichnung');
|
||||
|
||||
-- SEPA Mandates
|
||||
INSERT INTO public.sepa_mandates (account_id, member_id, mandate_reference, iban, bic, account_holder, mandate_date, status, sequence, is_primary) VALUES
|
||||
(v_account_id, v_m1, 'MNDT-2020-001', 'AT483200000012345864', 'RLNWATWW', 'Johann Maier', '2020-01-01', 'active', 'RCUR', true),
|
||||
(v_account_id, v_m2, 'MNDT-2020-002', 'AT611904300234573201', 'BKAUATWW', 'Maria Huber', '2020-01-01', 'active', 'RCUR', true);
|
||||
|
||||
-- Membership Applications
|
||||
INSERT INTO public.membership_applications (account_id, first_name, last_name, email, phone, street, postal_code, city, date_of_birth, message, status) VALUES
|
||||
(v_account_id, 'Maximilian', 'Ortner', 'max.ortner@example.at', '+43 664 9876543', 'Viaduktbogen', '6020', 'Innsbruck', '1997-08-14', 'Wurde von einem Mitglied empfohlen.', 'submitted'),
|
||||
(v_account_id, 'Hannah', 'Troger', 'hannah.troger@example.at', '+43 680 8765432', 'Erlerstraße', '6020', 'Innsbruck', '2001-03-22', 'Möchte gerne der Jugendabteilung beitreten.', 'submitted'),
|
||||
(v_account_id, 'Felix', 'Kirchmair', 'felix.kirchmair@example.at', '+43 660 7654321', 'Brennerstraße', '6020', 'Innsbruck', '1992-11-05', NULL, 'submitted');
|
||||
|
||||
END $$;
|
||||
|
||||
Reference in New Issue
Block a user