237 lines
15 KiB
TypeScript
237 lines
15 KiB
TypeScript
import { prisma } from '@innungsapp/shared'
|
|
import { format } from 'date-fns'
|
|
import { de } from 'date-fns/locale'
|
|
import Link from 'next/link'
|
|
import { toggleAiFeature } from './actions'
|
|
|
|
const PLAN_LABELS: Record<string, string> = {
|
|
pilot: 'Pilot',
|
|
standard: 'Standard',
|
|
pro: 'Pro',
|
|
verband: 'Verband',
|
|
}
|
|
|
|
const PLAN_COLORS: Record<string, string> = {
|
|
pilot: 'bg-gray-100 text-gray-700',
|
|
standard: 'bg-blue-100 text-blue-800',
|
|
pro: 'bg-purple-100 text-purple-800',
|
|
verband: 'bg-amber-100 text-amber-800',
|
|
}
|
|
|
|
const PAGE_SIZE = 20
|
|
|
|
export default async function SuperAdminPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ q?: string; page?: string }>
|
|
}) {
|
|
const { q = '', page = '1' } = await searchParams
|
|
const currentPage = Math.max(1, parseInt(page, 10))
|
|
const skip = (currentPage - 1) * PAGE_SIZE
|
|
|
|
const where = q
|
|
? {
|
|
OR: [
|
|
{ name: { contains: q, mode: 'insensitive' } },
|
|
{ slug: { contains: q, mode: 'insensitive' } },
|
|
{ contactEmail: { contains: q, mode: 'insensitive' } },
|
|
],
|
|
}
|
|
: {}
|
|
|
|
const [organizations, total] = await Promise.all([
|
|
prisma.organization.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
skip,
|
|
take: PAGE_SIZE,
|
|
include: { _count: { select: { members: true, userRoles: true } } },
|
|
}),
|
|
prisma.organization.count({ where }),
|
|
])
|
|
|
|
const totalPages = Math.ceil(total / PAGE_SIZE)
|
|
|
|
return (
|
|
<div className="max-w-[1400px] mx-auto space-y-12 py-4">
|
|
<div className="flex justify-between items-center">
|
|
<div className="text-left space-y-2">
|
|
<h1 className="text-3xl font-black text-gray-900 tracking-tight font-outfit">
|
|
Innungs-Verwaltung <span className="text-[#E63946]">PRO</span>
|
|
</h1>
|
|
<p className="text-gray-400 font-medium">Hierüber werden alle Mandanten der Lösung verwaltet.</p>
|
|
</div>
|
|
|
|
<Link
|
|
href="/superadmin/create"
|
|
className="bg-[#E63946] text-white font-bold py-3 px-6 rounded-xl hover:bg-[#D62839] transition-all shadow-md shadow-red-100 flex items-center gap-2"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2.5} stroke="currentColor" className="w-5 h-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
Neue Innung anlegen
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-12 items-start">
|
|
{/* List */}
|
|
<div className="space-y-6">
|
|
{/* Search & Filter */}
|
|
<div className="bg-white p-2 rounded-2xl border shadow-sm flex items-center">
|
|
<form method="GET" className="flex-1 flex gap-2">
|
|
<div className="relative flex-1 group">
|
|
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-gray-400 group-focus-within:text-[#E63946] transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-4 h-4">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
type="search"
|
|
name="q"
|
|
defaultValue={q}
|
|
placeholder="Innung suchen..."
|
|
className="w-full pl-9 pr-4 py-3 bg-transparent text-sm outline-none placeholder:text-gray-300"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="px-6 py-2.5 bg-gray-900 text-white rounded-xl text-sm font-bold hover:bg-black transition-all active:scale-[0.98]"
|
|
>
|
|
Suchen
|
|
</button>
|
|
{q && (
|
|
<Link
|
|
href="/superadmin"
|
|
className="p-2.5 bg-gray-50 text-gray-400 rounded-xl hover:bg-gray-100 transition-colors"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
</svg>
|
|
</Link>
|
|
)}
|
|
</form>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between px-2">
|
|
<h2 className="text-sm font-bold text-gray-400 uppercase tracking-widest">
|
|
Registrierte Innungen ({total})
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
{organizations.length === 0 ? (
|
|
<div className="bg-white p-12 text-center rounded-2xl border border-dashed border-gray-200">
|
|
<div className="text-gray-300 mb-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor" className="w-12 h-12 mx-auto">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 21v-7.5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75V21m-1.5 0H21m-8.47-17.69-6 6a.75.75 0 0 0-.215.53V21m1.5 0H1.875a.375.375 0 0 1-.375-.375V11.25c0-4.46 3.07-8.189 7.5-9.088a9 9 0 0 1 1.585-.152Z" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-gray-500 font-medium">
|
|
{q ? 'Keine Treffer für Ihre Suche.' : 'Bisher keine Innungen angelegt.'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
organizations.map((org) => (
|
|
<div key={org.id} className="group bg-white p-6 rounded-2xl border hover:border-[#E63946] hover:shadow-xl hover:shadow-red-500/5 transition-all duration-300 relative overflow-hidden">
|
|
<div className="flex justify-between items-start gap-6 relative z-10">
|
|
<Link href={`/superadmin/organizations/${org.id}`} className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="font-bold text-lg text-gray-900 group-hover:text-[#E63946] transition-colors">{org.name}</h3>
|
|
<span className={`text-[10px] font-black uppercase tracking-tighter px-2 py-0.5 rounded-full border ${PLAN_COLORS[org.plan] ?? 'bg-gray-100 text-gray-700'}`}>
|
|
{org.plan}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-xs text-gray-400 font-medium">
|
|
<div className="flex items-center gap-1.5 font-mono">
|
|
<span className="text-[#E63946]">@</span>
|
|
<span>{org.slug}</span>
|
|
</div>
|
|
<span className="w-1 h-1 rounded-full bg-gray-200" />
|
|
<span>{org.contactEmail || 'Keine Kontaktmail'}</span>
|
|
</div>
|
|
</Link>
|
|
|
|
<div className="flex items-center gap-2 lg:opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-2 group-hover:translate-x-0">
|
|
<form action={async () => {
|
|
'use server'
|
|
await toggleAiFeature(org.id, !org.aiEnabled)
|
|
}}>
|
|
<button
|
|
type="submit"
|
|
className={`p-2 rounded-xl border transition-all ${org.aiEnabled
|
|
? 'bg-green-50 text-green-600 border-green-100 hover:bg-red-50 hover:text-red-600'
|
|
: 'bg-gray-50 text-gray-400 border-gray-100 hover:bg-green-50 hover:text-green-600'}`}
|
|
title={org.aiEnabled ? 'KI Deaktivieren' : 'KI Aktivieren'}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.59 8.31m5.84 6.06a6.01 6.01 0 0 1-5.84-1.29m0 0a6.01 6.01 0 0 1 0-8.5l.08.08a6.01 6.01 0 0 1 0 8.42Z" />
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
|
|
<Link
|
|
href={`/superadmin/organizations/${org.id}`}
|
|
className="p-2 bg-gray-900 text-white rounded-xl hover:bg-black transition-all"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
</svg>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex items-center gap-6">
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Mitglieder</span>
|
|
<span className="font-bold text-gray-900">{org._count.members}</span>
|
|
</div>
|
|
<div className="w-px h-6 bg-gray-100" />
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Admins</span>
|
|
<span className="font-bold text-gray-900">{org._count.userRoles}</span>
|
|
</div>
|
|
<div className="w-px h-6 bg-gray-100 ml-auto" />
|
|
<div className="flex flex-col items-end">
|
|
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Erstellt am</span>
|
|
<span className="text-xs font-semibold text-gray-600">{format(org.createdAt, 'dd.MM.yyyy', { locale: de })}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<div className="pt-8 flex items-center justify-between border-t border-gray-100">
|
|
<span className="text-xs font-bold text-gray-400 uppercase tracking-widest">
|
|
Seite {currentPage} / {totalPages}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
{currentPage > 1 && (
|
|
<Link
|
|
href={`/superadmin?${new URLSearchParams({ q, page: String(currentPage - 1) })}`}
|
|
className="px-4 py-2 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-600 hover:bg-gray-50 transition-all active:scale-[0.98]"
|
|
>
|
|
← Zurück
|
|
</Link>
|
|
)}
|
|
{currentPage < totalPages && (
|
|
<Link
|
|
href={`/superadmin?${new URLSearchParams({ q, page: String(currentPage + 1) })}`}
|
|
className="px-4 py-2 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-600 hover:bg-gray-50 transition-all active:scale-[0.98]"
|
|
>
|
|
Weiter →
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|