214 lines
7.4 KiB
TypeScript
214 lines
7.4 KiB
TypeScript
import { prisma } from '@innungsapp/shared'
|
|
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
|
import { headers } from 'next/headers'
|
|
import { redirect } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import { MEMBER_STATUS_LABELS } from '@innungsapp/shared'
|
|
import { format } from 'date-fns'
|
|
import { de } from 'date-fns/locale'
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
aktiv: 'bg-green-100 text-green-700',
|
|
ruhend: 'bg-yellow-100 text-yellow-700',
|
|
ausgetreten: 'bg-red-100 text-red-700',
|
|
}
|
|
|
|
export default async function MitgliederPage(props: {
|
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
|
|
}) {
|
|
const searchParams = await props.searchParams
|
|
const search = typeof searchParams.q === 'string' ? searchParams.q : ''
|
|
const statusFilter = typeof searchParams.status === 'string' ? searchParams.status : undefined
|
|
|
|
const sanitizedHeaders = await getSanitizedHeaders()
|
|
const session = await auth.api.getSession({ headers: sanitizedHeaders })
|
|
if (!session?.user) redirect('/login')
|
|
|
|
const userRole = await prisma.userRole.findFirst({
|
|
where: { userId: session.user.id },
|
|
orderBy: { createdAt: 'asc' },
|
|
})
|
|
if (!userRole || userRole.role !== 'admin') redirect('/dashboard')
|
|
|
|
const members = await prisma.member.findMany({
|
|
where: {
|
|
orgId: userRole.orgId,
|
|
...(statusFilter && { status: statusFilter as never }),
|
|
...(search && {
|
|
OR: [
|
|
{ name: { contains: search } },
|
|
{ betrieb: { contains: search } },
|
|
{ ort: { contains: search } },
|
|
],
|
|
}),
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
})
|
|
|
|
// Also fetch admins to display them in the list if no status filter or status matches "aktiv"
|
|
const admins = await prisma.userRole.findMany({
|
|
where: {
|
|
orgId: userRole.orgId,
|
|
role: 'admin',
|
|
...(search && {
|
|
user: {
|
|
OR: [
|
|
{ name: { contains: search } },
|
|
{ email: { contains: search } },
|
|
]
|
|
}
|
|
})
|
|
},
|
|
include: {
|
|
user: true
|
|
}
|
|
})
|
|
|
|
const adminUserIds = new Set(admins.map((a: typeof admins[number]) => a.userId))
|
|
// Map userId → member record so admin entries show real member data
|
|
const memberByUserId = new Map<string, typeof members[number]>(members.filter((m: typeof members[number]) => m.userId).map((m: typeof members[number]) => [m.userId!, m]))
|
|
|
|
const combinedList = [
|
|
// Include admins only if there's no status filter, or if filtering for 'aktiv'
|
|
...(!statusFilter || statusFilter === 'aktiv' ? admins.map((a: typeof admins[number]) => {
|
|
const m = memberByUserId.get(a.user.id)
|
|
return {
|
|
id: m ? m.id : `admin-${a.user.id}`,
|
|
name: m?.name ?? a.user.name,
|
|
betrieb: m?.betrieb ?? a.user.email,
|
|
sparte: m?.sparte ?? 'Sonderfunktion',
|
|
ort: m?.ort ?? '—',
|
|
seit: m?.seit ?? null as number | null,
|
|
status: m?.status ?? 'aktiv',
|
|
userId: a.user.id,
|
|
isAdmin: true,
|
|
realId: m ? m.id : a.user.id,
|
|
role: 'Administrator',
|
|
}
|
|
}) : []),
|
|
...members.filter((m: typeof members[number]) => !adminUserIds.has(m.userId ?? '')).map((m: typeof members[number]) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
betrieb: m.betrieb,
|
|
sparte: m.sparte,
|
|
ort: m.ort,
|
|
seit: m.seit,
|
|
status: m.status,
|
|
userId: m.userId,
|
|
isAdmin: false,
|
|
realId: m.id,
|
|
role: 'Mitglied',
|
|
}))
|
|
]
|
|
|
|
combinedList.sort((a: typeof combinedList[number], b: typeof combinedList[number]) => a.name.localeCompare(b.name))
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Mitglieder</h1>
|
|
<p className="text-gray-500 mt-1">{combinedList.length} Einträge</p>
|
|
</div>
|
|
<Link
|
|
href="/dashboard/mitglieder/neu"
|
|
className="bg-brand-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-brand-600 transition-colors"
|
|
>
|
|
+ Mitglied anlegen
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white rounded-lg border p-4 flex gap-4">
|
|
<form className="flex gap-4 w-full">
|
|
<input
|
|
name="q"
|
|
defaultValue={search}
|
|
placeholder="Name, Betrieb, Ort suchen..."
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
|
/>
|
|
<select
|
|
name="status"
|
|
defaultValue={statusFilter ?? ''}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
|
>
|
|
<option value="">Alle Status</option>
|
|
<option value="aktiv">Aktiv</option>
|
|
<option value="ruhend">Ruhend</option>
|
|
<option value="ausgetreten">Ausgetreten</option>
|
|
</select>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200 transition-colors"
|
|
>
|
|
Suchen
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="bg-white rounded-lg border overflow-hidden">
|
|
<table className="w-full data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name / Betrieb</th>
|
|
<th>Rolle</th>
|
|
<th>Ort</th>
|
|
<th>Mitglied seit</th>
|
|
<th>Status</th>
|
|
<th>Eingeladen</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{combinedList.map((m) => (
|
|
<tr key={m.id}>
|
|
<td>
|
|
<div>
|
|
<p className="font-medium text-gray-900">{m.name}</p>
|
|
<p className="text-xs text-gray-500">{m.betrieb}</p>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium ${m.role === 'Administrator' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-700'}`}>
|
|
{m.role}
|
|
</span>
|
|
</td>
|
|
<td>{m.ort}</td>
|
|
<td>{m.seit ?? '—'}</td>
|
|
<td>
|
|
<span
|
|
className={`inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium ${STATUS_COLORS[m.status]}`}
|
|
>
|
|
{MEMBER_STATUS_LABELS[m.status as keyof typeof MEMBER_STATUS_LABELS] || 'Aktiv'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{m.userId ? (
|
|
<span className="text-[11px] font-medium text-green-600 bg-green-50 px-2 py-0.5 rounded-full">Aktiv</span>
|
|
) : (
|
|
<span className="text-[11px] text-gray-400">—</span>
|
|
)}
|
|
</td>
|
|
<td>
|
|
<Link
|
|
href={`/dashboard/mitglieder/${m.realId}`}
|
|
className="text-sm text-brand-600 hover:underline"
|
|
>
|
|
Bearbeiten
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{combinedList.length === 0 && (
|
|
<div className="text-center py-12 text-gray-500">
|
|
Keine Mitglieder gefunden
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|